2 Comments

Getting Started with AWS Lambda in F#

I recently set out to create my first AWS Lambda function. Of the supported languages, I chose my favorite: F#. In this post, I’ll walk through the process of building a Lambda function in F# and deploying it to AWS.

AWS Alphabet Soup

Many of Amazon’s web services have famously inscrutable names. After fumbling my way through the process of creating a function in the web UI (AWS Console), I retraced my steps in the command line to produce denser, more easily repeatable instructions:

AWS CLI

The first step is to install AWS CLI, which is available as awscli via Homebrew and pip.

Next, we’ll need user credentials, which brings us to our first acronym: IAM, or Identity and Access Management. Visit the IAM Console, create a non-root user if you don’t already have one, and then create an access key for it. Provide them to AWS CLI with aws configure (documentation). This should be the last time we need the web UI.

Before we continue, I’ll offer one tip upfront: When you’re referencing a local file, AWS CLI requires you to prefix it with file://. Weird.

Role and Policy

Time for the next two pieces of jargon: Our Lambda function will need a role with an associated policy.

The role creation subcommand (aws iam create-role) unfortunately requires you to provide a magic JSON blob in a file on disk. Avoid the hassle with a one-liner like this:


echo '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}' | aws iam create-role --role-name my-lambda-role --assume-role-policy-document file:///dev/stdin

(I acquired that JSON by creating a role in the UI and then fetching it with aws iam get-role.)

From the output, find the line that looks like this: "Arn": "arn:aws:iam::676570827806:role/my-lambda-role",

Time for another acronym! An ARN is an Amazon Resource Name, which is just an ID that you can unambiguously reference. Save it for later.

Next up, we’ll associate an existing managed policy with the role we created like this:


aws iam attach-role-policy --role-name my-lambda-role --policy-arn arn:aws:iam::aws:policy/AWSLambdaExecute

I chose AWSLambdaExecute arbitrarily for this toy project; check out the other available policies in the web portal to govern your function’s access to your various other AWS resources.

Creating the Function Project

With the AWS prerequisites wrapped up, it’s time for some code.

Tooling

I haven’t tried Lambda’s other supported languages, but for .NET languages, Amazon has created tooling that integrates nicely with the language’s ecosystem.

  1. First, install the latest .NET Core SDK if you don’t already have it.
  2. Next, install the Lambda project templates: dotnet new -i Amazon.Lambda.Templates
  3. Finally, install the Lambda tools: dotnet tool install -g Amazon.Lambda.Tools.

New Project

Scaffold a basic Lambda project with dotnet new lambda.EmptyFunction -lang f#. There are a handful of other Lambda templates available; see them with dotnet new -l.

The template doesn’t include a solution file. Create one with dotnet new sln, then associate the generated projects with dotnet sln add **/*.fsproj.

Take a look at the generated code (I recommend VS Code or JetBrains Rider). At the time of writing, the generated function uppercases an input string:


    member __.FunctionHandler (input: string) (_: ILambdaContext) =
        match input with
        | null -> String.Empty
        | _ -> input.ToUpper()

I think this is a bug. To work nicely with the provided tooling, change the parameter to a record type:


type Input = { foo : string }

type Function() =
    member __.FunctionHandler (input: Input) (_: ILambdaContext) =
        match input.foo with
        | null -> String.Empty
        | s -> s.ToUpper()

Deployment and Testing

Now we’re ready to deploy it! Thanks to the tools we installed above, deployment is as simple as dotnet lambda deploy-function my-lambda. It may ask for a region (e.g. us-east-2) and a profile ARN (from above).

Once it’s deployed, you can invoke your Lambda with test input like this:


jrr@jrrmbp ~/r/g/g/s/my-lambda (master) [255]> dotnet lambda invoke-function my-lambda --payload '{"foo":"jkl"}'
Amazon Lambda Tools for .NET Core applications (3.0.1)
Project Home: https://github.com/aws/aws-extensions-for-dotnet-cli, https://github.com/aws/aws-lambda-dotnet
 
Payload:
"JKL"
 
Log Tail:
START RequestId: 8305ef96-bef1-11e8-9704-9957a282268a Version: $LATEST
END RequestId: 8305ef96-bef1-11e8-9704-9957a282268a
REPORT RequestId: 8305ef96-bef1-11e8-9704-9957a282268a  Duration: 601.54 ms Billed Duration: 700 ms     Memory Size: 256 MB Max Memory Used: 61 MB

Billed for 700ms! Not bad.

You can also send test data to it from the web interface, as well as view logs, etc.

Impressions

After getting past the one-time administrivia in AWS, I was impressed by the developer tooling on offer. I appreciated having F# templates, something Microsoft’s own Azure Functions lack. The dotnet lambda global tool was good to see, especially since global tools have only existed for a few months.

I haven’t done anything meaningful with Lambda yet, such as wiring up input triggers or producing output side effects. But so far, I like what I see!

Further Reading