2023-12-13 16:46:45 -05:00
2023-12-12 21:51:48 -05:00
2023-12-12 21:51:48 -05:00
2023-12-12 21:51:48 -05:00
2023-12-12 21:51:48 -05:00
2023-12-12 21:51:48 -05:00
2023-12-12 21:51:48 -05:00
2023-12-12 21:51:48 -05:00
2023-12-12 21:51:48 -05:00
2023-12-12 21:51:48 -05:00

AWS Lambda transport for Swift OpenAPI

This library provides an AWS Lambda transport for Swift OpenAPI generator

This library allows to expose server side Swift OpenAPI implementation generated by the Swift OpenAPI generator as an AWS Lambda function.

The library provides two capabilities:

  • a default implementation of an AWS Lambda function in that consumes your OpenAPI service implementation
  • a binding with the Amazon API Gateway (HTTP API mode) (aka APIGatewayV2) event type.

Other Lambda function bindings (event types) are supported as well, depending on your needs. We include instructions to create a binding with an Amazon API Gateway (REST API mode)

Prerequisites

To write and deploy AWS Lambda functions based on an OpenAPI API definition, you need the following:

TL;DR

If you already have an OpenAPI definition, you already generated the server stubs, and wrote an implementation, here are the additional steps to expose your OpenAPI service implementation as a AWS Lambda function and an Amazon API Gateway HTTP API (aka APIGatewayV2).

If you don't know how to start, read the next section, there is a tutorial with step-by-step instructions.

To expose your OpenAPI implementation as an AWS Lambda function:

  1. Add the dependency to your Package.swift

    The project dependencies:

  dependencies: [
    .package(url: "https://github.com/apple/swift-openapi-generator.git", .upToNextMinor(from: "1.0.0-alpha.1")),
    .package(url: "https://github.com/apple/swift-openapi-runtime.git", .upToNextMinor(from: "1.0.0")),
    .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "1.0.0-alpha.1"),
    .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"),
    .package(url: "https://github.com/sebsto/swift-openapi-lambda", branch: "main") 
  ],

The target dependencies:

    .executableTarget(
      name: "YourOpenAPIService",
      dependencies: [
        .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
        .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
        .product(name: "OpenAPIRuntime",package: "swift-openapi-runtime"),
        .product(name: "OpenAPILambda",package: "swift-openapi-lambda"),
      ],
  1. Add a protocol and a constructor to your existing OpenAPI service implementation
import Foundation
import OpenAPIRuntime
import OpenAPILambda // <-- add this line 

@main
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi { // <-- add the OpenAPILambdaHttpApi protocol 

  init(transport: LambdaOpenAPITransport) throws { // <-- add this constructor (don't remove the call to `registerHandlers(on:)`)
    try self.registerHandlers(on: transport)
  }

  // the rest below is unmodified 

  func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {

    let symbol = input.path.symbol

    let price = Components.Schemas.quote(
        symbol: symbol,
        price: Double.random(in: 100..<150).rounded(),
        change: Double.random(in: -5..<5).rounded(),
        changePercent: Double.random(in: -0.05..<0.05),
        volume: Double.random(in: 10000..<100000).rounded(),
        timestamp: Date())

    return .ok(.init(body: .json(price)))
  }
}

  1. Package and deploy your Lambda function + create an HTTP API Gateway (aka APIGatewayV2)

🎉 Enjoy!

Tutorial (a Quick Start with a Stock Quote API service example)

Part 1 - the code

  1. Create a Swift executable project
mkdir quoteapi && cd quoteapi
swift package init --name quoteapi --type executable
  1. Write or import an OpenAI API definition in YAML or JSON
cat << EOF > Sources/openapi.yaml
openapi: 3.1.0
info:
  title: StockQuoteService
  version: 1.0.0
  
components:
  schemas:
    quote:
      type: object
      properties:
        symbol:
          type: string
        price:
          type: number
        change:
          type: number
        changePercent:
          type: number
        volume:
          type: number
        timestamp:
          type: string
          format: date-time
          
paths:
  /stocks/{symbol}:
    get:
      summary: Get the latest quote for a stock
      operationId: getQuote
      parameters:
        - name: symbol
          in: path
          required: true
          schema:
            type: string
      tags:
        - stocks
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/quote'
        400:
          description: Bad Request
        404:
          description: Not Found
EOF		  
  1. Add a Swift OpenAPI generator configuration file to generate only the server side
cat << EOF > Sources/openapi-generator-config.yaml
generate:
  - types
  - server
EOF 
  1. Use this Package.swift file to define targets and their dependencies
cat << EOF > Package.swift
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "QuoteService",
  platforms: [
    .macOS(.v13), .iOS(.v15), .tvOS(.v15), .watchOS(.v6),
  ],
  products: [
    .executable(name: "QuoteService", targets: ["QuoteService"]),
  ],
  dependencies: [
    .package(url: "https://github.com/apple/swift-openapi-generator.git", .upToNextMinor(from: "1.0.0-alpha.1")),
    .package(url: "https://github.com/apple/swift-openapi-runtime.git", .upToNextMinor(from: "1.0.0")),
    .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "1.0.0-alpha.1"),
    .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", branch: "main"),
    .package(url: "https://github.com/sebsto/swift-openapi-lambda", branch: "main") 
  ],
  targets: [
    .executableTarget(
      name: "QuoteService",
      dependencies: [
        .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
        .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
        .product(name: "OpenAPIRuntime",package: "swift-openapi-runtime"),
        .product(name: "OpenAPILambda",package: "swift-openapi-lambda"),
      ],
      path: "Sources",
      resources: [ 
        .copy("openapi.yaml"),
        .copy("openapi-generator-config.yaml")
      ],
      plugins: [
        .plugin(
            name: "OpenAPIGenerator",
            package: "swift-openapi-generator"
        )
      ]  
    ),
  ]
)
EOF
  1. Generate server side Swift stub of the OpenAPI API definition
swift build
  1. Replace main.swift with your own implementation
rm Sources/main.swift
cat << EOF > Sources/QuoteService.swift
import Foundation
import OpenAPIRuntime
import OpenAPILambda

@main
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi {

  init(transport: LambdaOpenAPITransport) throws {
    try self.registerHandlers(on: transport)
  }

  func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {

    let symbol = input.path.symbol

    let price = Components.Schemas.quote(
        symbol: symbol,
        price: Double.random(in: 100..<150).rounded(),
        change: Double.random(in: -5..<5).rounded(),
        changePercent: Double.random(in: -0.05..<0.05),
        volume: Double.random(in: 10000..<100000).rounded(),
        timestamp: Date())

    return .ok(.init(body: .json(price)))
  }
}
EOF
  1. Build the project to ensure everything works
swift build

Part 2 - the deployment

  1. Add the Lambda build instructions as a Docker file and a Makefile. We build for Swift 5.9 on Amazon Linux 2
cat << EOF > Dockerfile
# image used to compile your Swift code
FROM public.ecr.aws/docker/library/swift:5.9.1-amazonlinux2
RUN yum -y install git jq tar zip openssl-devel
EOF

cat << EOF > Makefile
### Add functions here and link them to builder-bot format MUST BE "build-FunctionResourceName in template.yaml"

build-QuoteService: builder-bot

# Helper commands
deploy:
	sam deploy 

logs:
	sam logs --stack-name QuoteService --name QuoteService 

tail:
	sam logs --stack-name QuoteService --name QuoteService --tail

######################  No Change required below this line  ##########################

builder-bot:
	$(eval $@PRODUCT = $(subst build-,,$(MAKECMDGOALS)))
	$(eval $@BUILD_DIR = $(PWD)/.aws-sam/build-swift)
	$(eval $@STAGE = $($@BUILD_DIR)/lambda)
	$(eval $@ARTIFACTS_DIR = $(PWD)/.aws-sam/build/$($@PRODUCT))
	
	# build docker image to compile Swift for Linux
	docker build -f Dockerfile . -t swift-builder

	# prep directories
	mkdir -p $($@BUILD_DIR)/lambda $($@ARTIFACTS_DIR)

	# compile application inside Docker image using source code from local project folder
	docker run --rm -v $($@BUILD_DIR):/build-target -v `pwd`:/build-src -w /build-src swift-builder bash -cl "swift build --static-swift-stdlib --product $($@PRODUCT) -c release --build-path /build-target"
	
	# create lambda bootstrap file
	docker run --rm -v $($@BUILD_DIR):/build-target -v `pwd`:/build-src -w /build-src swift-builder bash -cl "cd /build-target/lambda && ln -s $($@PRODUCT) /bootstrap"
  
	# copy binary to stage
	cp $($@BUILD_DIR)/release/$($@PRODUCT) $($@STAGE)/bootstrap
  
  	# copy app from stage to artifacts dir
	cp $($@STAGE)/* $($@ARTIFACTS_DIR)
EOF
  1. Add a SAM template to deploy the Lambda function and the API Gateway
cat << EOF > template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM Template for QuoteService

Globals:
  Function:
    Timeout: 60
    CodeUri: .
    Handler: swift.bootstrap
    Runtime: provided.al2
    MemorySize: 512
    Architectures:
      - arm64

Resources:
  # Lambda function
  QuoteService:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        # handles all GET / method of the REST API
        Api:
          Type: HttpApi
    Metadata:
      BuildMethod: makefile

# print API endpoint and name of database table
Outputs:
  SwiftAPIEndpoint:
    Description: "API Gateway endpoint URL for your application"
    Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
EOF
  1. Build the Lambda function executable for Amazon Linux 2
sam build
  1. Deploy the Lambda function and create an API Gateway in front of it
# use --guided for the first deployment only. SAM cli collects a few parameters and store them in `samconfig.toml`
sam deploy --guided 

This command outputs the URL of the API GAteway, for example:

Outputs                                                                                                                     
-----------------------------------------------------------------------------------------------------------------------------
Key                 SwiftAPIEndpoint                                                                                        
Description         API Gateway endpoint URL for your application                                                           
Value               https://747ukfmah7.execute-api.us-east-1.amazonaws.com                                                  
-----------------------------------------------------------------------------------------------------------------------------
  1. Test your setup
curl https://747ukfmah7.execute-api.us-east-1.amazonaws.com/stocks/AAPL
{
  "change" : -4,
  "changePercent" : -0.030052760210257923,
  "price" : 111,
  "symbol" : "AAPL",
  "timestamp" : "2023-12-13T03:12:35Z",
  "volume" : 63812
}

Local Testing

git clone https://github.com/sebsto/swift-openapi-lambda.git && cd swift-openapi-lambda
# In the directory of the Swift OpenAPI Lambda transport project
LOCAL_LAMBDA_SERVER_ENABLED=true swift run 

# from another terminal, in the directory of the QuoteAPI sample project
curl -v -X POST --header "Content-Type: application/json" --data @events/GetQuote.json  http://127.0.0.1:7000/invoke

Implement your own OpenAPILambda to support other event types

TBD

References

Swift OpenAPI generator

To get started with the Swift OpenAPI generator, check out the full documentation, which contains a step-by-step tutorial.

Swift on AWS Lambda

The Swift Runtime for AWS Lambda allows you to write AWS Lambda functions in the Swift programming language.

To get started, check out this step-by-step tutorial and the documentation.

Serverless Application Model (SAM)

Read "What is SAM" to understand and get started with SAM.

S
Description
An AWS Lambda transport for Swift OpenAPI
Readme 8.1 MiB
Languages
Swift 89.3%
Shell 10.6%