Black Rock Blog

Zero to AWS Lambda in Scala | BKS2

Zero to AWS Lambda in Scala

AWS Lambda is the platform for deploying functions to the AWS cloud. You can use it to develop functions that respond to AWS events (eg S3 uploads, DynamoDB inserts), AWS API calls, or via HTTP endpoints using the API Gateway.

In this tutorial you’ll learn how to create a new Lambda function in Scala, accessible via an API Gateway HTTP endpoint but also testable locally with Docker. This tutorial has been tested with Scala 2.12.8, SBT 0.13.18, and SAM-CLI 0.15.0.

Update Since publishing this post I have found that setting the Lambda function memory size to 512 (mb) provides a significant reduction in response times for a “cold start” request. See the Measuring AWS Lambda Response Times post for details.

Requirements

This tutorial uses SBT for building Scala projects and Amazon’s SAM CLI app for testing, packaging, and deploying AWS Lambda functions. You’ll need all of the following to complete the tutorial:

  1. SBT • The Scala Build Tool
  2. SAM CLI • The AWS Serverless Application Model app (a wrapper around AWS CloudFormation and the CLI)
  3. AWS CLI • The popular CLI for accessing the AWS API
  4. Docker • Required by SAM for local testing of Lambda functions
  5. curl • The performant web client used to test the running Lambda functions
  6. An AWS account and valid credentials

The Project

Getting The Project

Download and extract the aws-lambda-hello-scala repository.

It’s a small repo, so if you want to get more familiar with the layout of Scala projects you can opt to browse the repo and type them in yourself.

Along with the standard Git project files (.gitignore and README.md), there are three build files (build.sbt, project/assembly.sbt, and project/build.properties), a SAM configuration file (template.yaml), a Java entrypoint (ApiGatewayProxyHandler.java), and the single Scala function (ApiHandler.scala). The important files are described in more detail in the following section.

Got the project? Run sbt assembly to build & package & verify you’re ready for the rest of the tutorial. The project specifies the version of Scala and SBT used so the assembly step should “just work”.

About The SBT Build

The build.sbt file includes the project name, Scala version, and the two Java AWS-Lambda libraries used to handle the incoming API Gateway events. These lines show the two Java library dependencies:

build.sbt
6
7
8
9
libraryDependencies ++= Seq(
"com.amazonaws" % "aws-lambda-java-events" % "2.2.6",
"com.amazonaws" % "aws-lambda-java-core" % "1.2.0"
)

The sbt-assembly plugin (added in project/assembly.sbt) is used to build the full application and all dependent libraries into a single “fat” JAR file. This line defines the name of the fat JAR file generated by sbt-assembly:

build.sbt
4
assemblyJarName in assembly := "hello-scala.jar"

Finally the build.properties file defines the SBT version used, 0.13.18.

About The Java Entrypoint

The AWS Lambda platform defines a number of available runtimes including Node.js, Python, Go and Java, but not one for Scala directly. Fortunately SBT does compile Java files, so we can use a Java class as our entrypoint and then immediately invoke the Scala function.

Here is the single Java function handleRequest(). It takes the API Gateway request and context, calls our Scala function, and returns a standard API Gateway response.

src/main/java/lambda/ApiGatewayProxyHandler.java
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* Handle a Lambda request via the API Gateway
* @param requestEvent the HTTP request
* @param context info about this lambda function & invocation
* @return an HTTP response
*/
public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent
requestEvent, Context context) {

ApiHandler.Response response = ApiHandler.handle(requestEvent, context);

return new APIGatewayProxyResponseEvent()
.withBody(response.body())
.withStatusCode(response.statusCode())
.withHeaders(response.javaHeaders());
}

Our Scala function, ApiHandler.handle(), will take the Java APIGatewayProxyRequestEvent and Context instances and return a Scala ApiHandler.Response which this function handily copies into the APIGatewayProxyResponseEvent return value.

As an improvement, one could make this entrypoint even friendlier to our Scala function and convert the Java arguments and their Java collections to Scala instances and collections. I left that part out (there’s a lot of data to convert in those instances!), so the Scala code will have some odd-looking Java collection calls.

About The Scala Handler

And now for the good part! Here is the single Scala function handle() which takes the aforementioned Java instances and returns an instance of the case class Response.

src/main/scala/lambda/ApiHandler.scala
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
object ApiHandler {

/**
* Handle a Lambda request indirectly via the API Gateway
* @param request the Java HTTP request
* @param context the Java Lambda context
* @return the HTTP response
*/
def handle(request: APIGatewayProxyRequestEvent, context: Context): Response = {

println("handling %s %s, remaining time is %d ms".format(
request.getHttpMethod, request.getPath, context.getRemainingTimeInMillis) )

println(s"""environment = ${sys.env.getOrElse("env", "n/a")}""")

val name = request.getPathParameters.get("name")
Response(s"Hello, $name\n", Map("Content-Type" -> "text/plain"))
}

case class Response(body: String, headers: Map[String,String], statusCode: Int = 200) {
def javaHeaders: java.util.Map[String, String] = headers.asJava
}

}

This function handles the business logic for our sample Lambda function, namely:

  1. Printing out some useful information about the request and the context, including the amount of time remaining until the Lambda execution times out.
  2. Printing out an environment variable that we’ll set in the template.yaml SAM configuration file, just to demonstrate how they can be used.
  3. Retrieving the name path parameter passed to our function
  4. Returning a response including the body (“Hello, “) and content type.

You’ll be able to view the output of these println statements when running the function, both locally and in AWS. The Context instance does provide its own logging features, but since Scala’s println works I’d rather keep things simple and log output the Scala way.

Note the Response.javaHeaders() helper function in the case class. This allows us to use concise Scala syntax to build the map of HTTP headers while providing a java.util.Map collection for use in the Java API response instance.

About The SAM Configuration

The SAM CLI can use a single yaml file to define both the AWS Lambda function and the API Gateway endpoint configuration. This is a wonderfully simple way to define a Lambda function, where the API Gateway is implicitly configured from the Lambda function(s) definition. A more explicit approach is to also define the API Gateway endpoints in this file with an OpenAPI Specification (a.k.a Swagger), but for a simple function this will seem like a duplication of effort.

Note: a drawback to using the streamlined implicit configuration is that (as of SAM CLI 0.15.0) the API Gateway’s StageName variable will be set to its default value of Prod and thus be set in the first section of the external endpoint path . If you want a single function definition that can be deployed to multiple stages and not have Prod in the path you’ll need to add the API Gateway section with an OpenAPI specification to the file.

Here is the full SAM configuration with the streamlined implicit API Gateway configuration:

template.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
AWSTemplateFormatVersion: '2010-09-09'

Transform: AWS::Serverless-2016-10-31

Description: >-
A SAM CLI template for a single-endpoint Hello World Lambda function in Scala

Resources:
HelloScalaFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: HelloScala
Description: A simple AWS Lambda function in Scala
Runtime: java8
Handler: lambda.ApiGatewayProxyHandler
CodeUri: target/scala-2.12/hello-scala.jar
Timeout: 15
Environment:
Variables:
env: staging
Events:
Hello:
Type: Api
Properties:
Path: /hello/{name}
Method: GET

Outputs:
ApiURL:
Description: "API endpoint URL for Prod environment"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/{name}"

Some of the important features to point out:

  1. As is standard for Java Lambda functions the template specifies the java8 runtime, the full canonical (package and name) of the Java entrypoint, and the path to the “fat” JAR file.
  2. The max return time of the Lambda function is increased from the default 3 seconds to 15 seconds to give the Lambda engine enough time to cold-start this Java application (typically about 10 seconds).
  3. SAM will hard-code the API Gateway’s StageName to Prod which figures into the deployed endpoint path. To work around this inability to change the StageName, I’ve introduced a separate environment variable simply named env as an example of an alternate strategy for communicating development stages to your deployed functions.
  4. The ApiURL output variable is using CloudFormation’s substitution function to build the eventual url of the Lambda function. We’ll use the AWS CLI to read this generated url after deploying the function to AWS.

Execution

At this point you should have downloaded or otherwise created the “aws-lambda-hello-scala” project and are ready to try it out. We’ll be running and testing out the Lambda function first locally in Docker and then remotely deployed to AWS.

Running The Function Locally

Open your terminal to the project directory and run the following to build and run the function locally:

$ sbt assembly && sam local start-api

The sbt assembly command to compile the function and package it in a “fat” JAR file could be run by itself, but since sam local depends on this step it’s easier and clearer to combine them and then fail immediately if there is a compilation problem.

Open a separate terminal in the same directory and test out the function with curl:

$ curl http://127.0.0.1:3000/hello/developer
Hello, developer

The Hello, $name response from the function appears, with the name taken from the path parameter following the hello/ segment.

Switch back to the first terminal (the one running sam local start-api) and you’ll see the output from the Scala function’s println statements.

handling GET /hello/developer, remaining time is 14202 ms
environment = staging

In addition to the method and path, the request instance contains methods for accessing the headers, query string parameters, the POST body…. in short, everything you’ll need for an HTTP endpoint.

Now that the function is verified as working locally, it’s time to deploy it to AWS Lambda!

Deploying The Function To AWS

The deployment process requires interaction with AWS APIs and building infrastructure, and so is complicated enough to warrant being explained in multiple steps.

Create An S3 Bucket

To package and deploy your function to AWS you’ll need an S3 Bucket to hold the packaged (zipped) function code. Head over to the AWS S3 Console and create a new S3 bucket or select an existing S3 bucket to hold the code during deployment. Save the bucket name (just the name, not the full S3 URI) for the upcoming deploy step.

Upload Your Function

Run the following command to package and upload the function to your S3 bucket. You should already have the SAM CLI app installed (see the requirements section above).

$ sam package --s3-bucket <your-s3-bucket-name> --output-template-file packaged.yaml

Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /<path>/packaged.yaml --stack-name <YOUR STACK NAME>

The “Uploading” progress meter should finish up at 100%, followed by this instruction you can disregard about deploying the package with cloudformation (a reminder that the SAM CLI is essentially a utility wrapper around AWS CloudFormation and the AWS CLI). The result of this deployment is a packaged version of your function sitting in S3 and a generated packaged.yaml CloudFormation file ready for deployment.

Pick An AWS Region

While S3 buckets are global, AWS Lambda functions and API Gateway endpoints are regional. Before deploying your function you will need to choose your AWS Region. When you have chosen a region (perhaps one close to you, or with a preferred pricing model) and its region code (eg “us-west-2”), run the following command to set your region for AWS operations:

$ export AWS_DEFAULT_REGION=<region-code>

Deploy Your Function

Run the following command to deploy your function from S3 to AWS Lambda in your selected region:

$ sam deploy --template-file packaged.yaml --stack-name aws-lambda-hello-scala --capabilities CAPABILITY_IAM 

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - aws-lambda-hello-scala

After about a minute you should see the confirmation message that the CloudFormation stack “aws-lambda-hello-scala” has been created (and executed!). You now have an API Gateway endpoint and Lambda function deployed.

Retrieve the Endpoint URL

The next step is to find the url of that new endpoint. Fortunately an output command was included in the template.yaml (and thus packaged.yaml) to print out the new url (see the About The SAM Configuration section above).

Run the following command to describe the CloudFormation stack including the rendered url:

$ aws cloudformation describe-stacks --stack-name aws-lambda-hello-scala 

"Stacks": [
    {
        "StackId": "arn:aws:cloudformation...:stack/aws-lambda-hello-scala/...",
        "StackName": "aws-lambda-hello-scala",
        "ChangeSetId": "arn:aws:cloudformation:...:changeSet/awscli-cloudformation-package-deploy..",
        "Description": "A SAM CLI template for a single-endpoint Hello World Lambda function in Scala",
        "StackStatus": "CREATE_COMPLETE",
        "Outputs": [
            {
                "OutputKey": "ApiURL",
                "OutputValue": "https://f2c0a1aa55.execute-api.us-west-2.amazonaws.com/Prod/hello/{name}",
                "Description": "API endpoint URL for Prod environment"
            }
        ],..
    } 
}                

Test Your Function

It’s time to try out your function running in AWS Lambda! You can use the same curl command and “name” argument as you used last time, plus the time command to measure the function’s performance.

Run the following command, using the actual hostname in the OutputValue field returned from your previous command:

$ time curl https://f2c0a1aa55.execute-api.us-west-2.amazonaws.com/Prod/hello/developer

Hello, developer
curl   0.02s user 0.01s system 1% cpu 10.57 total

You can see the response is correct, but the performance (at 10+ seconds) isn’t that great. This is an example of the famous “AWS Lambda Cold Start” issue where the first execution after a deploy, or the first execution after the function has “slept” after a short period of unused, includes time to “wake up” the function and start the Java runtime.

Run the command again a few times and you’ll see far more reasonable response times:

$ time curl https://f2c0a1aa55.execute-api.us-west-2.amazonaws.com/Prod/hello/developer

Hello, developer
curl   0.02s user 0.01s system 1% cpu 0.23 total

$ time curl https://f2c0a1aa55.execute-api.us-west-2.amazonaws.com/Prod/hello/developer

Hello, developer
curl   0.02s user 0.01s system 1% cpu 0.15 total

When the AWS Lambda Java runtime is fired up and hasn’t been put to sleep after a certain amount of inactivity the response time is quite reasonable - here returning in 230ms and 150ms. If you do not expect your function to have regular periods of inactivity, or find occasional responses of 10 seconds after inactivity acceptable, then you should be all set. Otherwise, there are recommendations available for dealing with the “cold starts” that include sending “wake-up” traffic at regular intervals to avoid the Java runtime “sleeping” and other solutions.

As you recall from testing the function locally, the Scala handler code included a few println() commands to log the function’s request and context information. Fortunately in AWS Lambda these statements have been logged to AWS Cloudwatch.

You can view your function’s logs by running the following command:

$ sam logs -n HelloScalaFunction --stack-name aws-lambda-hello-scala

Found credentials in shared credentials file: ~/.aws/credentials
START RequestId: 3426d1e6-3843-434a-8564-a5a6509aad93 Version: $LATEST
handling GET /hello/developer, remaining time is 14999 ms
environment = staging
END RequestId: 3426d1e6-3843-434a-8564-a5a6509aad93
REPORT RequestId: 3426d1e6-3843-434a-8564-a5a6509aad93    Duration: 1.81 ms    Billed Duration: 100 ms     Memory Size: 128 MB    Max Memory Used: 115 MB    

This is very much like the output from running the AWS Lambda function locally (in docker), with the extra information about the API Gateway request.

Congratulations! You have now verified your AWS Lambda function is deployed and working correctly!

Wrapping Up

Decommissioning Your Lambda Function

I hope you’ll continue to experiment with writing AWS Lambda functions in Scala. This particular function you’ve deployed is only really useful for learning and testing. You could choose to let it stay deployed and keep an eye on the relatively-inexpensive costs with AWS Cost Explorer. I prefer to shut down AWS resources when I’m done with them, as I tend to forget about them when I don’t use them.

When you are ready to shut the AWS resources deployed in this article (besides the S3 bucket you may have created), you can do so by deleting the CloudFormation stack defined by template.yaml. This will remove the API Gateway endpoint, undeploy your AWS Lambda function, and (obviously) remove the CloudFormation stack.

Run this command to delete the CloudFormation stack and its resources:

$ aws cloudformation delete-stack --stack-name aws-lambda-hello-scala

After a few seconds you can check with CloudFormation to verify the stack was deleted:

$ aws cloudformation describe-stacks --stack-name aws-lambda-hello-scala 

An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id aws-lambda-hello-scala does not exist

More directly, you can try to hit your endpoint with the curl command again and verify it fails:

$ curl https://f2c0a1aa55.execute-api.us-west-2.amazonaws.com/Prod/hello/developer

# while the hostname is still active but the endpoint / function are decommissioned
{"message": "Internal server error"} 
# after the hostname has been decommissioned
curl: (6) Could not resolve host: f2c0a1aa55.execute-api.us-west-2.amazonaws.com

Next Steps

The 10-second “cold start” problem with the Java runtime in AWS Lambda functions may be a blocker for many developers. Although it is possible to keep the Java runtime active and awake, this does incur a (minor) additional cost for the traffic. A potential solution worth exploring is using the GraalVM to compile a Scala function to a native executable and then invoke it with the custom AWS Lambda runtime. The startup time for a GraalVM native Scala app should be in the milliseconds instead of ~10 seconds and remove the “cold start” problem altogether. One challenge is that the custom runtime requires extra work to use, as the function needs to do the work of fetching requests instead of being called from an entrypoint.

Another improvement you could explore is adding a AWS::Serverless::Api section to the template.yaml file in which you can override the StageName to something other than “Prod”. This could be useful for maintaining separate deployment environments where the environment name is included in the url. For example you could define stages that keep your deployed versions separate (eg /dev/hello/{name} vs /staging/hello/{name} vs /prod/hello/{name}).

Conclusion

The AWS SAM CLI provides a straightforward way to build, test, and deploy AWS Lambda functions in Scala (albeit with a Java entrypoint). You can reuse the aws-lambda-hello-scala example project as a starting point for your own projects and explore the limits of serverless functions!