Developer Tools • macOS • 12:22
Serverless functions are increasingly becoming popular for running event-driven or otherwise ad-hoc compute tasks in the cloud, allowing developers to more easily scale and control compute costs. Discover how to use the new Swift AWS Lambda Runtime package to build serverless functions in Swift, debug locally using Xcode, and deploy these functions to the AWS Lambda platform. We’ll show you how Swift shines on AWS Lambda thanks to its low memory footprint, deterministic performance, and quick start time.
Speaker: Tom Doron
Downloads from Apple
Transcript
Hello, and welcome to WWDC. Hello. My name is Tom Doron and today I'm thrilled to share with you a set of technologies that allow you to build and debug serverless functions written in Swift using Xcode. Many systems these days have client components like iOS, macOS, tvOS or watchOS applications as well as server components. The server components enable the client applications to extend their functionality into the cloud, for example: access data that is not available on the device, offload tasks that can be done in the background, or offload tasks that are computational heavy.
Often, server components need to be built using different tools and different methodologies, creating a separation between the server and the client engineering teams. Serverless functions offer a programming model that brings the two closer together. Serverless functions are becoming an increasingly popular choice for running event-driven or otherwise ad hoc compute tasks in the cloud. They resolve the need in running dedicated resources by replacing them with a more dynamic resource allocation system. In many cases, serverless functions allow developers to more easily scale and control the compute costs given their on demand and elastic nature.
AWS Lambda is an event-driven, serverless computing platform provided by Amazon as part of Amazon Web Services, and it is considered among the industry leaders in this space. When building systems with serverless functions, extra attention is given to compute resource utilization as it directly impacts the overall cost of the system.
The combination of developer friendliness and low resource footprint makes Swift a fantastic choice for building serverless functions. Given how well these two go together, we're happy to offer a Swift solution for building and debugging serverless functions in Xcode and deploying them to AWS Lambda. Let me show you what this looks like.
And this is it. Only four lines of code. Let's review the API in detail. First, we import the AWS Lambda Runtime library. Next, we call Lambda.run and pass in a closure that takes a context, an event payload and a completion handler. The function can call the completion handler when the work is done. The closure will be invoked as event payloads become available for it to process, and the runtime library will manage the program's life cycle and interaction with the underlying platform.
There is also a second protocol oriented API that is designed for performance sensitive use cases. This API exposes the SwiftNIO EventLoop underpinning, which allow the Lambda function to share the same thread as the networking processing stack and so to avoid context twitches. This API is more powerful, but it comes at a cost of cognitive and technical complexity as the Lambda function needs to take care to never block the EventLoop.
In most cases, closure based Lambda functions are the right choice. And in this example, you may have noticed that we're using a request and response struct that conform to the Codable protocol, providing easy serialization to and from JSON. In most cases, Lambda payloads are JSON based, so this represents a more typical use of the library. Now, let's see how to build and debug a Lambda function, and for that, we will switch over to Xcode.
In this example, which is included in the Swift AWS Lambda Runtime library repository, we have a workspace with two projects: a Lambda and an iOS app. The Lambda is a package manager project with an executable product and the dependency on the Swift AWS Lambda Runtime library. If we look at the Lambda main.swift, which is the entry point for the program, we can see a request struct and a response struct similar to the ones we saw on the slide. The request struct is a user registration form with a name and a password field, and the response is a message with a simple greeting to the new user.
Switching over to the iOS app, we can see a similar setup. The SwiftUI registration form with a text field for the username and a secure field for the password and a button that invokes a register function. The question, of course, is how do we tie these two things together? How does a register function cause the Lambda function on the other side? Let's try to run our Lambda function and see what happens.
We hit "run" on the Lambda target over here, and we can see that it failed. Looking at the error, it could not fetch work from the Lambda runtime engine because the connection was refused. This makes sense because we're not running in AWS Lambda Runtime. We're running in Xcode. Luckily, the library comes with a special mode that enables a Simulator that simulates the AWS runtime engine. We can turn this on by editing the scheme and setting an environment variable called LOCAL_LAMBDA_SERVER_ENABLED to true. Let's do that and try to run the Lambda again.
In this case, we're getting much better results. We're seeing that the Local Lambda Server was started and is listening on local host port 7000 and receiving events on the invoke endpoint. That's pretty sweet. It also gives us a hint as to what we can do on the iOS application side. Let's switch over there and try to write our register function.
For this, we will use a snippet we prepared up front, and this snippet uses URLSession to the local host address provided to us by the Lambda function in sending the request form, and then handle the response. We can add a couple breakpoints, maybe when we're constructing the request, and as we are handling the response, we can also add a breakpoint on the Lambda function. Let's run our iOS application in the Simulator and see what happens.
We can see that Xcode is managing two processes. One for the Lambda function and one for the iOS application. We can also see that the Lambda only takes 6.3 megabytes of memory. That's pretty sweet. Now, let's switch to the application in the Simulator and try to register. Put in our name and our password and click the register button. Then we hit the first breakpoint. This is the construction of the request. This all looks pretty good. Let's remove this breakpoint and continue.
And now we hit the second breakpoint on the Lambda function. Here we can inspect, maybe, the request that we got from the client, and we see our username and supersecret password. That looks good as well. Remove the breakpoint and continue. And finally, we hit the last breakpoint when processing the response. We can use the LLDB debugger to examine the response as well.
Finally, we get our greeting showing up on the UI as expected. Awesome, but how did this work? We used Xcode to manage two processes for our two executable targets: the iOS application and the Lambda function. To make the Lambda function available over HTTP, we also started a local HTTP server that simulates the AWS Lambda Runtime engine. We used a special environment variable to turn this functionality on.
The iOS application then used an HTTP call, using URLSession to submit work to the Lambda. This setup only works locally in debug mode. To interact with a Lambda deployed to AWS over HTTP, you need to expose it first through AWS API Gateway. Now, let's see how to deploy our Lambda function. And for this, we will switch over to the terminal.
To deploy our Lambda function, we can use a variety of tools provided by AWS, such as SAM or AWS CLI. In this example, which is also available as part of the library code on GitHub, we build a small script that wraps the AWS CLI and shows you the different steps required to deploy the Lambda function to AWS. Let's run scripts/deploy and see what it does.
First, we create a Docker image that is based on the Amazon Linux 2 image published by Swift.org. Next, we compile the Lambda function in the Docker container. Then we package the executable and all of its dependencies in a .zip file, which is the expected package format. Finally, we upload the .zip file to AWS and notify about the new code version. With the code updated, we can also test our Lambda function using the AWS CLI wrapper script.
We're gonna call scripts/test and see what happens. We enter our username and password and we get a greeting as expected. Sweet! But how did this work? When we deploy the Lambda function to AWS, the AWS Lambda Runtime engine will control the program's life cycle. The Lambda polls the runtime engine for work, and if such work is not available, the compute resource will be hibernated until work is available.
The iOS application, command line tool or any other client can use an HTTP call, using URLSession or otherwise, to interact with the Lambda. To make the Lambda function available as an HTTP endpoint, you can use AWS API Gateway which routes every request it receives to the Lambda function queue. There are also other ways to invoke Lambda functions, such as event based triggers, which are covered by AWS's documentation.
In order to make all this possible, we needed to create two main pieces of technology. The first part was getting Swift to run on Amazon Linux, a flavor of CentOS. Starting May 2020, Swift.org has begun publishing Swift toolchains for building and running Swift programs on Amazon Linux 2. The toolchain is useful in the context of many AWS compute services, including EC2 and AWS Lambda.
The second part was building a Swift Lambda runtime library, an implementation of the AWS Lambda Runtime API. The library provides a multitier API that allows building a range of serverless functions, from quick and simple closures to complex and performance sensitive event handlers. A program's life cycle is managed by a lifecycle loop provided as part of the library.
The program is designed to serve traffic forever, or until the process is terminated by the AWS platform. Long-lived processes can serve traffic faster by employing caching techniques such as caching the connection. The library also manages a state machine representing the various stages of the Lambda execution. Polling work from the runtime engine queue, submitting the work to the user provided function, and submitting the results back to the runtime engine.
An asynchronous HTTP client that is fine tuned for performance in the AWS Lambda Runtime context is embedded in the library, and compiling the Lambda program produces an executable that links the user provided code with the underlying runtime library and the Swift dependencies. And they can be linked together statically or dynamically, depending on the need. AWS Lambda can then be configured to run as many copies of the serverless functions as required, and this elasticity means that this simple programming model can be scaled up to meet even significant demand.
This also means that the serverless functions are designed to be stateless and care needs to be taken to avoid global mutable state or holding on to memory for longer than required. Finally, the runtime library includes extensible integration points to trigger serverless functions based on events. For example, events from AWS systems like S3, SQS and SNS, triggers based on HTTP endpoints exposed via API Gateway, or custom events that can be added by the user.
To wrap up, serverless functions are a great way to extend your iOS, macOS, watchOS, tvOS or any other client applications to the cloud. Swift is a perfect match for serverless functions. It is fast, safe and uses only little resources. The tools you need to build, debug and run Swift based serverless functions are available today. So, what are you waiting for? Go build awesome things. Thank you for listening, and enjoy WWDC.