In part 1 we got our feets wet with some initial simple Hello Cloud scripts, using F#. This was then expanded to do actual connectivity to AWS and list S3 buckets in an AWS account.
In this part, we will sidetrack a bit into the topic of developer workflow and then continue with more exploration of AWS services and get a bit more into F#, with scripts to retrieve server information. This will be a step-but-step process, start simple and add features.
My description here aims at documenting a workflow where the script is developed, since I do believe that it is important to establish an (enjoyable) approach to develop code, in whatever language that may be. In this case, it is F#.
IDE integration with F# Interactive REPL
In part 1 I mentioned only briefly about different IDEe (integrated development environments) with F# support and my own preference with Jetbrains tools. I think a key element of a good developer workflow is a fast feedback loop and ease of experimentation - a REPL-type environment like FSI is essential for this. For me, I think the REPL-driven development process in Clojure has set kind of a gold standard for how things should be in that area. Functional languages provide a good foundation for allowing such workflows, given the focus on immutability and functional composition.
In both Jetbrains Rider, Visual Studio and Visual Studio Code (with the Ionide F# plugin) you can send an expression to FSI through a simple keyboard shortcut or menu command and the expression will be evaluated in FSI and you can also interact with the FSI REPL directly. This makes it quite easy to interact and experiment with code as you write it and can get essentially immediate feedback.
It is a little bit different still from what I am used to with Clojure, but quite good and useful to fiddle around and test things.
Below is a very simple example with Hello Cloud of interaction with the REPL in Visual Studio Code, which I have switched to from Jetbrains Rider for the time being. For F# script development, I found the VS Code experience more enjoyable.
- I can select the hello function and then press Alt-Enter to send the selection to F# Interactive - which is started automatically if it is not already running. The selection is evaluated in the REPL.
- I have a few example calls at the bottom of the file, inside a comment block. If I press Alt-Enter without selecting anything, it will send the contents of the line to the REPL and it gets evaluated and I see the result immediately.
- This way there is a kind of scratch area for doing immediate feedback testing, which also is persisted
This is not a replacement for actual unit testing test suites, but a nice low overhead complement.
Using F# 5 preview features
In part 1 we took advantage of a new feature of the upcoming F# 5, which allows us to specify dependencies to our scripts, which will be downloaded by the .NET package manager (NuGet) automatically, if needed.
This is quite handy when developing standalone scripts for performing tasks towards our cloud provider
of choice, like our simple
HelloS3.fsx script in the previous blog post. While it worked fine to run
these scripts, we did not look at how this was supported in our developer workflow.
If you had worked with these scripts in some IDE with F# support, it may not have understood these new commands
for referencing dependencies.
The trick here is to enable the language preview setting for FSI in the IDE. In Jetbrains Rider 2020.2 EAP I enabled the language preview, but in that case, it still did not understand the dependencies I had referenced in the scripts. In Visual Studio Code this worked better though, the same option as specified in the command line can be added in the settings for the Ionide F# extension, see picture below.
The settings change will not take effect immediately, so be patient.
AWS credentials handling
Next part in enabling a workflow in the IDE with the F# Interactive REPL for communicating with a cloud provider is how to access the credentials set up. Previously we relied on being able to set environment variables to be able to access the correct AWS profile.
We can do the same in the IDE. However, I did not find a way to configure which environment variables to set when an F# Interactive REPL process is started and we just send selected code to the REPL. It is possible to set environment variables like AWS_PROFILE by setting the environment variable when we start the IDE, e.g. when starting VIsual Studio Code from the command line. Processes started by Visual Studio Code will inherit the environment variables. So it can be started like this:
The path to Visual Studio Code needs to be added to the search path for the command line first also. It is not ideal, but works.
It is also possible to add code in the scripts to set the necessary environment variable or fetch credentials from an AWS profile explicitly
Adding a line
before instantiating any client object would be one simple way to get the same effect - but probably not hardcode the value… It is a manageable problem anyway.
Note: The function Environment.SetEnvironmentVariable is something that is provided by .NET Core and is not something specific to F#. The way the function is called can give a hint there. It takes two parameters, but instead of being called
it is called as
It is not two parameters that are sent to the function call, but instead a single parameter which is a tuple with two values. Since the other official .NET languages do not support partial function application like F# do and have to provide all required parameters in a call, I guess this is the approach taken in F# to interoperate with functions adapted for those other languages.
Our next task here is to show some information about virtual machines in AWS (a.k.a. EC2 instances). The first iteration should be simple, just show something about every EC2 instance in an AWS account in a specific region. We have the AWS SDK for .NET API docs and we can do a similar starting point that we did with
HelloS3.fsx in part 1.
The structure is fairly similar to our previous S3-based example - create a client object for EC2 and call the function that should return EC2 instance information. At this point, we also just return that result. An addition is to take a string array as arguments, which we could pass to the script if called, and use the first argument as the AWS profile name, if it is provided.
The workflow I use here is that the code here is sent from the editor to the F# Interactive REPL and try out various things as the code is developed. The comment block at the end contains a few expressions that I add there while developing and testing the code.
So we can use one expression with the REPL to call showServers and see if it works (see below).
We can use the autocompletion of the editor (VS Code) to see what fields we have in the response and check what is in there. It seems from the first tests here that the call to AWS was successful and we have a field Reservations which is a list of Reservation - and that list seems to be empty. Which in this case is correct, since the account and region I connected to does not have any EC2 instances at all.
So time to create some…
EC2 instances created
I created two EC2 instances and now when calling the function and inspecting the result, this is the result:
There are two Reservation and each seems to contain a sequence of Instance, which should be the actual machines. The rest of the information under Reservation we do not bother with for the time being. So we iterate over the list of Reservation objects and in each of these, we get all the Instance objects. The result from that should be a single list of Instance objects.
A new update of the code adds this type of functionality:
The Reservations list is converted to a generic F# list and then provided as input to the new function getInstances.
This function calls the List.collect function, which iterates over a list and calls a function for each element
in the list. The result from that function call is expected to be a list itself. This list of lists is then flattened to a single list.
The function that is called is an anonymous function which we have defined using the fun keyword as
(fun x -> List.ofSeq x.Instances). The function takes a single argument, which in our case is a Reservation object.
From this object, we retrieve the list of instances from the Instances field and convert that to a generic F# list and
return that as the result of the function.
In our case we have also added a condition to only call getInstances if the reservations list is not empty, otherwise, we return an empty list of type Instance. With two instances we can see that we get some appropriate data back.
Extracting instance informastion
So now when we see that we can get Instance objects it is time to extract some information out of it. There is a lot of information available for an instance, but not all of that may be useful at first glance. A couple of things comes to mind that could be useful:
- The id of the instance
- The name of the instance, if that exists
- The type of instance (EC2 has type names which have specific properties such as CPU, RAM etc)
- The private IP address and/or DNS name
- When it was launched
- The current state (running, stopped etc)
We can expand on that list later, but this looks like a reasonable starting point. So we should then have some kind of record in which we can store the information we want for each instance. A first stab at defining such a record structure could be:
Most of it is strings to start with right now. We could arguably refine this further and we will, but this is what we start with. For this then, we need a function that creates an InstanceInfo record from an Instance object. For all these fields except the Name field, this is trivial. The Name field is the more tricky one since it is not mandatory and is represented through a tag on the instance which is called “Name”. So first just skip the Name field issue and leave it empty and get an initial version. Notice here that the curly brackets are used to group the fields of the InstanceInfo record instance we create - it is not any code brackets as in some other languages! Notice also that we have not specified anywhere that this is an InstanceInfo that is returned - F# figures that out by itself given the shape of the record.
and test that in the REPL:
so now deal with the issue with the Name field - if we keep the Name field as an empty string if the Name tag does not exist, then we need to iterate through the list of tags and if there is a tag with its key-value “Name”, then we take the value of that tag and set that as the Name field.
Finding the Name tag
Let us break down what we want to do here into a few steps:
- We want to be able to iterate over all tags, so we want an iterable structure. We have a .NET:y list right now, but want to make it a more F#:y one. It could be handled already as in F# sequence, or we could have as an F# list or an F# array.
- Once we have the iterable structure, we want to go through it and for each element check if it is the Name tag. If that is the case, we return the value of the tag. if we do not find the Name tag, we return an empty string.
- We use the result of the search above to initiate the Name field returned by getInstanceInfo.
Note here that we want to work with immutable data, so some constructs that may be more common in other object-oriented and imperative languages we will not use - even if F# technically allows us to do that. One example there is that we will not create a default structure of InstanceInfo with an empty Name field and then after that populate it with a different value. We will set the value to the correct one right from the start.
Among the EC2 instances I created, I happened to name one of them “Tiny Instance”. We can create a list of Tag objects and check the content in the REPL:
Cool, the first instance was that particular EC2 instance it seems! How do we then find the value of the Name tag? It does not have to be in the first position in the list, even though it is so in this case. The List module has a few functions that we could use to find it - one way would be to get the length and then iterate over each element using an index to access each element. This is not optimal, even though this may be a way to approach it in some other languages.
Another way though it breaks down the problem into primary cases:
- What do we do if get an empty list? We return an empty string.
- What do we do if the first element in the list is the Name tag? We return the value of that tag.
These two cases we can handle using what is called a match expression. Let us construct a function getTagValue which handles those naive cases:
The match expression is a quite powerful construct. Between match and with the expression that should be matched is put. Then there is a list of pattern expressions to match that with, prefixed with | operator and after the -> operator the result expression that is to be executed if there is a match is put.
So in this case we specify en empty list (  ) and if there is a match for that, we return an empty string. The second case was if the first item in the list was the name tag. In the pattern expression we specify the cons operator ( :: ) with two names, one on each side - head and tail. The cons operator is a way to describe a list as the first element and the rest of the list. In this has, head is the first element in the list and tail is the rest of the list. F# will do this matching and assign the first element to the name head and the rest of the list to the name tail.
So we can then check if the first element is the Name tag and if so, return the value. A Tag object has two fields, one named Key and one named Value. So we simply check if the Key field is equal to the tag name we are looking for and then return its value. Otherwise, we return the empty string.
This seems to work just fine:
So that is great and we have solved the problem for two easy cases. But what if the list of tags has multiple tags and the Name tag is not at the first position? Well, we can then just simply extend our solution slightly. In our pattern matching, we had the situation where the second pattern would give us the rest of the list. This means if we apply the same logic to the rest of the list, we will either return an empty string if the rest of the list is empty or return the tag value if the Name tag is the first element of the rest of the list. I.e. we get a recursive function, which can call itself. There is only a tiny bit of modification to make this work:
We add a rec keyword after the let keyword to indicate that the function will be recursive and then we simply just call the function again in the else clause. Done! Now we can find the Name tag at any position in the list if it exists. We then can update getInstanceInfo function to use this:
and the complete script at this point is then:
Show the result
Now we can get the result as a list of InstanceInfo objects. We can look at the result from that in the REPL, but would perhaps be a bit nicer with some kind of print option. So we can make a function to print the contents of the InstanceInfo list. A simple way to print stuff to the screen that we already have used is the printfn function, so let us use that. We can print strings fine with printfn, but there is no built-in formatting code for DateTime objects. So we can make a simple function to convert the DateTime to string, in a format we prefer. I like the yyyy-mm-dd hh:mm:ss format, so the formatting function should do that:
Next, let us make a function using printfn that prints some headers for the fields and then the content of the InstanceInfo list, one item per row. The function is not expected to return anything, we just want the side-effect of printing. Therefore to clarify this the return type of the function is declared as unit. It is similar to void in some other languages, but there is an actual value for that type - not an absence of type as in some other languages.
First handle the case if the list is empty, then take care of non-empty lists. There we print a heading first and then iterate over the list, printing each item. The heading is created in the same way as the items, using the same approach to make it easier to manage to format. This works well:
Note though that the format string is actually not a regular string type, so it cannot be replaced with a simple named value string.
We tweak the main function showServers a bit to use this function also and then we get the current result of the script as this:
We can call the new showServers from the REPL:
or we can run the script from the command line:
The trouble with types
The type we have used in our script to represent instance information (InstanceInfo) is for the most part using just plain strings to represent different values. With the very limited scope, our script has now, that might not be a big issue. But if we go much beyond what we have now, then this may potentially become a problem once the codebase grows larger. We have a few different fields in InstanceInfo that are not just plain strings:
- The InstanceId is an identifier with a specific format of the string, e.g. “my dog is cute” would not be a valid value here.
- InstanceType is actually a string representation of multiple entities, each with a limited range of values
- PrivateIpAddress will only be something with a valid IP address representation. e.g. “18.104.22.168”.
- State will also have a limited set of values it can have, e.g. “running”, “stopped”, “terminated” etc.
The good thing is that the type system in F# can help us catch issues that might happen if we would just use primitive type values like strings for everything. It is beyond the scope of this post to build out full-blown types with validation of formats and all possible cases for all the fields we included, but I want to include just a very simple addition that at least makes it clear in code what a string should represent if we use it.
Let us take the PrivateIpAddress field. This is an IP address, so let us make an IPAddress type and use that instead in our InstanceInfo record:
The type declaration for IpAddress may look a bit strange - does it somehow refer to itself? The IpAddress to the left of the equal sign is the type name. The IpAddress on the right side of the equal sign is part of the type value. It essentially says that a value of the type IpAddress must consist of an identifier named IpAddress and then a value of type string.
We then change the type of the PrivateIpAddress field to use this. If you try to run the script now, it will fail and you will have multiple errors. The first one is where we assign the value to this field in the function. Instead of:
we will not be able to assign the string from
instance.PrivateIpAddress directly, we need to
tell it that the string is to be used for the IpAddress type. So we need to add the identifier that is part of the type value:
This is then good for the assignment of the value. But there will still be errors elsewhere, the call to print the values will fail, because now the value to print is not a string type, but an IpAddress type!
One way we can address this (there are multiple ways) is to define a conversion function from our IpAddress type to a string and then use that function when we are going to print the value. This is a simple one-liner, which we put next to our type definition so we have them grouped:
The toString function takes one parameter that consists of two parts, the identifier IpAddress and s. Then the function will simply return s. Given that our type IpAddress is defined with an identifier IpAddress and then a string value, the compiler will know that s will be of type string in this case.
Then we need to change our printfn call to use this conversion function:
This is a small change to make it a bit more explicit in the code that it is an IP address we are dealing with. So now our complete script looks like this:
Is that worth the effort? Let us say that we want to expand our code to include either a private IP address or a DNS name, but not both. Can we do that in a way that let us be clear what is there and capture potential issues?
Let us change our type IpAddress to be a type that can represent either and IP address or a DNS name. We can do that easily:
Now we have type IpAddressOrDns that have two possible type values; either an IP address or a DNS name. Each of these cases is represented by an identifier (IpAddress or DnsName) and then a value of string type.
Once we create this change you will see that F# will report an error on the toString function we defined, which likely will look like the one in the picture below:
The F# compiler knows that this pattern is from our IpAddressOrDns type and it also knows that you have not handled all cases of what that type can be, what case you have not taken care of here and tells you what it is.
So we need to update toString to handle both cases:
We do not specify the cases we have in the parameter declaration, we just set a parameter name. Instead, we rely on pattern matching in the function itself. We do not need to specify any type information here explicitly, since the compiler knows what type v is and what type s is since it knows how the type IpAddressOrDns is defined. The code compiles now since we have covered all possible cases for the IpAddressOrDns type.
We need to change the type of the field in InstanceInfo from IpAddress to IpAddressOfDns and then the script will work again. But we may also want to rename the field PrivateIpAddress to perhaps PrivateHostAddress, if that is a field that actually should be able to contain either an IP address or a DNS name.
With this update the script code looks like this:
That is it for now!
Side note: If you get an error like this below, it means that whatever function you are calling have been compiled against a different version of the record than you are passing to it, when you are interacting with the REPL. This would not be an issue in Clojure, but a statically typed language this may happen in the REPL interaction.
In that case you need to make sure you re-evaluate all the code pieces that have been affected by your type change!
After struggling a bit with getting an enjoyable experience in developing F# scripts, I decided to focus a bit more on workflow aspects in this post, before digging a bit deeper into cloud use cases.
I believe that a good REPL and good use of a REPL is a key factor to an enjoyable development workflow. Clojure is really great in that area and F# grows on me as well here. I think both of these languages do a good job here and they are both perrhaps more focused on an editor<->REPL integration workflow, more than extended interactive sessions in the REPL itself.
Other languages which I think have pretty nice REPL experience include Julia and R. If there are further improvements to the REPL experience for F# I think both of these may serve well as inspiration. Julia was probably inspired by R in what to include in the REPL, is my guess.
I hope this was at least somewhat useful for some of you!
In part 3 we are going to start with some F# code using AWS Lambda.
The source code in the blog posts in this series is posted to this Github repository:
Author Erik Lundevall-Zara
LastMod 2020-09-20 (e5ee6b9)