AWS Lambda Tutorial for Swift: Getting Started

Swift is now available in the world of serverless functions via AWS Lambda! Get started deploying your first on-demand serverless function with our AWS Lambda for Swift tutorial. By Ralph Kuepper.

Leave a rating/review
Download materials
Save for later
Share

Swift is now available in the world of serverless functions, specifically Amazon Web Services (AWS) Lambda. In this AWS Lambda Tutorial for Swift, you’ll write a simple currency conversion function that takes a value and returns the corresponding value for another currency.

Lambda is a platform provided by AWS that allows functions to run on demand. Multiple functions with a sole purpose support an application by responding to events as they happen — resources are computed, a server is spun up and your code runs using AWS resources. This is an excellent solution for applications with unpredictable workloads and scalability requirements.

Your project will be a serverless function for AWS Lambda using Swift. Along the way, you’ll learn how:

  • Swift runs on AWS Lambda
  • To set up your AWS account
  • Writing an AWS Lambda function in Swift works
  • To connect your AWS Lambda function to the world
Note: This tutorial assumes you have some basic knowledge of Swift and you know how to work with related Swift tools (like Xcode). If you don’t, check out Programming in Swift: Fundamentals. Additionally, you need an AWS account ready to go. If not, go here and sign up: https://aws.amazon.com. Be aware that costs may apply.

Getting Started

Download the starter project by clicking on the Download Materials button at the top or bottom of this tutorial.

You’ll also want to install Docker or use its web interface when prompted later in the tutorial.

Before you start writing actual code, you’ll need to set up your AWS account and its services. If you’re using an account that’s already in use for other projects or has customized settings, you may need to tweak a few settings and permissions. But, every new account will automatically create all the permissions you need for this tutorial.

You’ll need to set up the following services:

  • AWS Lambda
  • API Gateway

AWS Lambda is the platform this function will run on, while API Gateway connects the function to the world wide web.

Creating your AWS Lambda Function

Start by setting up your AWS Lambda function. Navigate to the Lambda section within AWS. When you first log in to AWS, you’ll find it either in the list of suggested services or by selecting it in the main menu, like this:

Finding Lambda in AWS after logging in.

Note: If you don’t find AWS Lambda right away, you can search for it by typing Lambda in the search box.

Click on Create function and make sure you have Author from scratch selected.

Now fill out the fields of this form with the following values:

  • Function name: eurCurrencyConverter
  • Runtime: Provide your own bootstrap on Amazon Linux 2
  • Permissions: Create a new role with basic AWS Lambda permissions

Creating a new lambda function

Click on Create function and wait for AWS to create the function. You can leave the function alone for now.

Creating Your API Gateway

Next, you’ll create an API Gateway to use for interacting with the AWS Lambda function.

Click on Services and select API Gateway. If you have trouble finding it, you can type in the beginning of API Gateway, as shown below:

Finding API Gateway in AWS

On the API Gateway, create a new HTTP API by selecting Build in the HTTP API section:

Create an HTTP API Gateway

When prompted, name the API “eurConversion” and click Add integration. In the drop-down, select AWS Lambda, and fill out the fields to match your existing AWS Lambda function — AWS will auto-complete for you. It should look like this when done:

Create an HTTP API Gateway

Note: Your AWS Region may be different, and the AWS Lambda Version may have changed since this article was written.

Configuring Your API’s Routes

Click Next and configure the routes. Set the path to /convert and set the integration target to the newly configured AWS Lambda integration. Leave the method as ANY since the function serves POST and GET requests.

AWS API Gateway - configure routes

Click Next again and then define stages like so:

AWS API Gateway, Define stages

Click Next one more time, confirm your API settings by clicking Create, and your API is ready.

Now that you’ve created a home for your AWS Lambda function, it’s important to understand how Swift and AWS Lambda work together.

Using Swift with Lambda and AWS

AWS Lambda supports a couple programming languages natively. This means you can upload the source code directly and AWS Lambda can compile it on the fly. Unfortunately, this isn’t yet the case for Swift. So, for Swift to run on AWS Lambda, you need to:

  1. Compile the function as an executable file that runs on Amazon Linux 2.
  2. Include all dependencies and libraries with the bootstrap file.

You’ll use Docker to do that, and it’ll provide you with a convenient ZIP file of your function and its dependencies that you upload to AWS Lambda.

AWS Lambda does come with a few limitations:

  • AWS Lambda functions run for a maximum of 15 minutes.
  • A function may take a few extra seconds to run for the first time since AWS Lambda is booting the function. This is also referred to as a “cold start”.
  • AWS Lambda is, by definition, stateless; there’s no shared memory among AWS Lambda functions.
  • AWS Lambda functions can perform a variety of tasks, so not every AWS Lambda function is a public function. Depending on your use case, you may not even need internet access.
  • AWS Lambda functions can use EventLoops, but they’re usually used within a specific context only.

Swift has released the official AWS Lambda runtime, which provides a convenient framework to develop lambda functions. This runtime is the basis for the function you’ll develop in this tutorial.

Testing AWS Lambda Functions Locally

When you develop AWS Lambda functions, testing them is a little different than developing regular applications. All functional tests involve calling the URL: http://localhost:7000/invoke as a POST request. Internally, AWS Lambda is calling the function with parameters depending on the setup. If you’re using AWS Lambda with a trigger from an SQS queue, you’ll deal with different parameters than when using it behind API Gateway.

Note: To enable the local testing server, set an environment variable, LOCAL_LAMBDA_SERVER_ENABLED, with a value of true for the runtime to start in this mode. You can do this by editing your project’s Run scheme. This isn’t necessary to complete this tutorial since you’ll be using your Lambda API Gateway.

Now it’s time to write some code.

Writing a Swift App for AWS Lambda

Open the starter project in Xcode by clicking on Package.swift. Take note of the following two dependencies in this file:

// 1
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git",   
         .upToNextMajor(from:"0.1.0")),
// 2
.package(url: "https://github.com/swift-server/async-http-client.git", 
         from: "1.0.0")

The two dependencies are:

  1. The application needs the AWS Lambda function to have the official AWS Lambda framework available.
  2. The AsyncHttpClient to fetch a document from a server

Next, take a look at main.swift in Sources ▸ EURCurrencyRate. This file defines three global instances of classes you need in your functions:

//1
let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
//2
let httpClient = HTTPClient(eventLoopGroupProvider: .createNew)

defer {
  try? httpClient.syncShutdown()
}

Here’s what’s happening in the code above:

  1. JSONEncoder and JSONDecoder will read and write JSON content.
  2. You need HTTPClient to pull the most current exchange rate from an external website. You’re creating a new EventLoop by using the .createNew static value for the eventLoopGroupProvider parameter when initializing your HTTPClient.
Note: AWS Lambda isn’t running on an EventLoop by default and it doesn’t need to.

At line 50, you see the function that AWS Lambda calls through the runtime framework:

Lambda.run { 
  (context, 
   request: APIGateway.V2.Request, 
   callback: @escaping (Result<APIGateway.V2.Response, Error>) -> Void) in

The framework defines input and output types you use in the function and return in the form of a callback function. The context of the request is defined as APIGateway.V2.Request, which will tell you what kind of a request this is.

You also see a global convert function, which will create the actual output of the AWS Lambda function on line 46.

You’ll get this function to react to the following two requests:

  • GET: /convert: Take the input in form of query parameters: ?amount=1
  • POST: /convert: Take the input in the form of a JSON body.

Creating Your Function’s Model

Before you write the logic, create the following three types in separate files in EURCurrencyRate:

First, create RateInput.swift for a struct that resembles the input for the POST call:

import Foundation
 
struct RateInput: Codable {
  // 1
  let amount: Double
  // 2
  let currency: String?
}

The two inputs are:

  1. amount: This is the actual amount of money the function converts.
  2. currency: This is optional and will default to USD.

Now create a RateOutput.swift for a struct that represents the output of the conversion:

import Foundation
 
struct RateOutput: Codable {
  // 1
  let inputAmount: Double
  // 2
  let rate: Double
  // 3
  let inputCurrency: String
  // 4
  let outputAmount: Double
  // 5
  let outputCurrency: String
}

The variables in this struct represent:

  1. inputAmount: the original amount provided
  2. rate: the applied conversion rate
  3. inputCurrency: the input currency for inputAmount
  4. outputAmount: the converted amount, using inputAmount and rate
  5. outputCurrency: the desired currency

Finally, create a RateResponse.swift file containing the structure for the response from the API you use to determine the exchange rate:

import Foundation
 
struct RateResponse: Codable {
  var rates: [String: Double]
  var base: String
}

The API returns many rates; the logic will sort out which one to take.

Writing Your Function’s Body

Next you’ll write the business logic. Write the following code in the Lambda.run function in main.swift:

// 1
switch (request.context.http.path, request.context.http.method) {
// 2
case ("/convert", .GET):
  // 3
  let amount = Double(request.queryStringParameters?["amount"] ?? "0.0") ?? 0
  let desiredCurrency = request.queryStringParameters?["currency"] ?? "USD"
  // 4
  convert(amount: amount, desiredCurrency: desiredCurrency, callback: callback)
// 5
case ("/convert", .POST):
  // 6
  if let data = request.body?.data(using: .utf8), 
    let input = try? jsonDecoder.decode(RateInput.self, from: data) {
      convert(
        amount: input.amount, 
        desiredCurrency: input.currency ?? "USD", 
        callback: callback)
  } else {
    // 7
    callback(.success(APIGateway.V2.Response(statusCode: .badRequest)))
  }
default:
  // 8
  callback(.success(APIGateway.V2.Response(statusCode: .notFound)))
}

Here’s what the code above does:

  1. Checks the context of this call. Remember AWS Lambda functions are often called internally and not always by a user and a browser.
  2. If it’s a GET call to /convert, this case applies.
  3. Since it’s a GET call, convert the query parameters into variables that you can pass to the convert function.
  4. Call the convert function and pass the callback along.
  5. If it’s a POST call, this case applies.
  6. Instead of parsing the variables from the URL, you decode the data from the request body into the RateInput struct. If this succeeds, call the convert function as well.
  7. If, for some reason, like invalid JSON, the body doesn’t parse, return a 400: Bad Request HTTP code.
  8. If it’s a call to a different path or a different type of HTTP call, return a 404: Not Found HTTP code.

Both functions then call the global convert function to calculate the output and return it to the runtime.

Write the following code in the convert function:

// 1
httpClient.get(url: "https://api.exchangeratesapi.io/latest").whenComplete { 
  result in
  switch result {
  case .failure(let error):
    // 2
    callback(.failure(error))
  case .success(let response):
    // 3
    if let data = response.body, response.status == .ok {
      let data = try? jsonDecoder.decode(RateResponse.self, from: data)
      if let data = data {
        // 4
        for currency in data.rates.keys where currency == desiredCurrency {
          // 5
          let rate = Double(data.rates[currency] ?? 0)
          // 6
          let newAmount = rate * amount
          // 7
          let output = RateOutput(
            inputAmount: amount,
            rate: rate,
            inputCurrency: "EUR",
            outputAmount: newAmount,
            outputCurrency: desiredCurrency)
          // 8
          if let data = try? jsonEncoder.encode(output) {
            // 9
            callback(.success(APIGateway.V2.Response(
              statusCode: .ok,
              multiValueHeaders: ["content-type": ["application/json"]],
              body: String(decoding: data, as: Unicode.UTF8.self)
            )))
          }
        }
      }
      callback(.success(APIGateway.V2.Response(statusCode: .badRequest)))
    } else {
      callback(
        .success(APIGateway.V2.Response(statusCode: .internalServerError))
      )
    }
  }
}

Here’s what’s happening in the code above:

  1. You call an API that returns conversion rates.
  2. If this fails, you return an error right away, as you can’t calculate anything.
  3. At this point, you at least have a response from the API, so now you parse it into the previously defined RateResponse struct.
  4. You loop through the different rates to find the one specified in the input.
  5. Convert the value to a double.
  6. Calculate the new amount.
  7. Form the output as the RateOutput struct.
  8. Convert this output to JSON.
  9. Call the callback function with the JSON string and a 200: OK HTTP code.

This function is the heart of your program. Once you write this, you can start compiling! :]

Getting the Function Running on AWS

OK, it’s time to get the function up and running. To deploy to AWS Lambda, compile your application into a binary file that can run on AWS Lambda. The best way to do this is by compiling it on the correct Linux distribution, which you can do through Docker. Amazon Linux 2, the runtime behind AWS Lambda, is available for Swift, and so the command to build your application using Docker looks like this:

docker run \
  --rm \
  --volume "$(pwd)/:/src" \
  --workdir "/src/" \
  swift:5.2-amazonlinux2 \
  /bin/bash -c "yum -y install libuuid-devel libicu-devel libedit-devel libxml2-devel sqlite-devel python-devel ncurses-devel curl-devel openssl-devel libtool jq tar zip && swift build --product EURCurrencyRate -c release && scripts/package.sh EURCurrencyRate"

This commands does the following:

  • run a Docker container. --rm tells Docker to delete the container when the container finishes.
  • Use your src folder in the working directory ($(pwd)) as a volume.
  • Set /src/ as the work directory (–workdir).
  • Use the Amazon Linux 2 image with Swift pre-installed.
  • Then run commands to install required dependencies and compile the code. Afterward, run a small script, package.sh, that Apple provided as part of the AWS Lambda runtime framework. It creates a ZIP file that contains all the files AWS Lambda needs.

Compiling using Docker complete!

Run the command from above and wait until the script finishes. Finally, run the following command:

cp .build/lambda/EURCurrencyRate/lambda.zip .
Note: This copies the resulting lambda.zip file into your main folder so you can grab it from there.

Uploading Your Function to AWS Lambda

Now head back over to AWS Lambda and click on the function to open it there. Select Upload a .zip file in the Function code section. Choose the ZIP file you created and upload it. And that’s it! Your lambda function is ready now.

Upload a zip file to the lambda function.

Go back to the API Gateway you created by clicking services and searching for “API Gateway.” Open the API Gateway resource you created and find the “Invoke URL” section.

Open the invoke URL in your API

Copy and paste the URL into your browser and append /convert?amount=10.00. You’ll see the desired output like this:

The output you expect from the AWS Lambda function.

Your first lambda function is live and deployed! :]

Note: If you receive an internal server error, there may be an error with your account’s permissions. This can usually be addressed by following the troubleshooting steps in the Amazon Knowledge Center.

Using Additional Services With AWS Lambda

If you’ve worked with AWS before, you’re well aware of other services you might have an interest in. The ones that are often used in connection with AWS Lambda are SNS, SQS and S3. For those — but also for many other AWS services — check out Soto. It’s the AWS SDK for Swift and can integrate nicely with the Swift Lambda Runtime.

It’s likely you’ll need some configuration via stored credentials in your AWS Lambda function. The easiest way to do this is by pulling these credentials from a Parameter Store through Soto. That way, your credentials aren’t saved in any files but stay in the cloud.

You may also want to use AWS Lambda functions in a different context — for example, as background workers. You can use most of the Server-Side Swift packages available to connect to databases.

Where to Go From Here?

You can download the final project by clicking the Download Materials button at the top and bottom of this page.

In this tutorial, you learned how to create an AWS Lambda function using Swift. Swift has a growing ecosystem of packages and projects, and AWS Lambda is compatible with many of them. If you want to explore writing web applications that might run on AWS Lambda with a more holistic framework, check out Server-Side Swift with Vapor.

If you’re new to web development but have experience with Swift, you’ll find it’s easy to create robust, fully featured web apps and web APIs with Vapor 4.

If you have any questions or comments, please join the forum discussion below!