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][aws-plain-english] 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][aws-cli-configuration]). 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](https://aws.amazon.com/blogs/developer/best-practices-for-local-file-parameters/) 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.
- First, install the latest [.NET Core SDK](https://www.microsoft.com/net/download) if you don’t already have it.
- Next, install the Lambda project templates:
`dotnet new -i Amazon.Lambda.Templates` - 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][vs-code-fsharp] 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][template-issue]. 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][Lambda Console], 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](https://spin.atomicobject.com/2018/05/17/azure-functions-f-sharp/). The `dotnet lambda` global tool was good to see, especially since global tools have only existed for a [few months](https://blogs.msdn.microsoft.com/dotnet/2018/05/30/announcing-net-core-2-1/#net-core-tools).
I haven’t done anything meaningful with Lambda yet, such as wiring up input [triggers](https://docs.aws.amazon.com/lambda/latest/dg/invoking-lambda-function.html) or producing output side effects. But so far, I like what I see!
## Further Reading
- [What’s the AWS IoT Suite Like? – A Dozen Lego Kits Mixed Together](https://spin.atomicobject.com/2018/05/21/aws-iot-component-overview/)
- [Amazon Lambda Auto-Deployment For Your Alexa Skill Using AWS CLI](https://spin.atomicobject.com/2017/01/25/alexa-aws-deployment/)
- [Managing AWS CloudFront Security Group with AWS Lambda](https://spin.atomicobject.com/2016/03/01/aws-cloudfront-security-group-lambda/)
[lambda-support-fsharp]: https://aws.amazon.com/blogs/developer/f-tooling-support-for-aws-lambda/
[aws-plain-english]: https://www.expeditedssl.com/aws-in-plain-english
[dotnet-global-tools]: https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools
[aws cli]: https://aws.amazon.com/cli/
[aws console]: https://aws.amazon.com/console/
[aws-cli-configuration]: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-quick-configuration
[role]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html
[policy]: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html
[IAM Console]: https://console.aws.amazon.com/iam
[managed policy]: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#aws-managed-policies
[vs-code-fsharp]: https://docs.microsoft.com/en-us/dotnet/fsharp/get-started/install-fsharp?tabs=macos#install-f-with-visual-studio-code
[jetbrains rider]: https://www.jetbrains.com/rider/
[supported languages]: https://docs.aws.amazon.com/lambda/latest/dg/lambda-app.html#lambda-app-author
[template-issue]: https://github.com/aws/aws-lambda-dotnet/issues/321
[record type]: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/records
[Lambda Console]: https://console.aws.amazon.com/lambda
[administrivia]: https://en.wiktionary.org/wiki/administrivia
[arn]: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
Interesting–is Atomic Object using F# in any of its professional services or was this a personal fun project?
Yes 😉
This post was from a personal fun project, but we also use F# on client projects.