Skip to content

Commit 01dd45c

Browse files
author
Jan Kammerath
committed
added some documentation
1 parent 19eca6f commit 01dd45c

File tree

4 files changed

+61
-5
lines changed

4 files changed

+61
-5
lines changed

README.md

+30-3
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,28 @@
22

33
This is a boilerplate application that shows how to run a serverless Swift application on AWS Lambda using AWS SAM. The Lambda operates on Linux running on AWS Graviton2 CPUs with an arm64 architecture. It uses a local proxy to interact with the [Vapor](https://vapor.codes/) framework. This code is part of my article [Serverless Swift With Vapor On AWS Using AWS SAM And Lambda](https://medium.com/@jankammerath/serverless-swift-with-vapor-on-aws-using-aws-sam-and-lambda-3bd89bed5325). If you're interested in running Swift code on AWS Lambda, this may serve as boilerplate.
44

5-
## A note on Vapor
5+
## How to use it yourself
66

7-
The vapor integration curently uses the `AsyncHTTPClient` and thus the local loopback on the Lambda instance. Vapor is initialized when the Lambda container first starts making the cold start take around 800-900ms on a 128 MB arm64 container running Amazon Linux 2. There is no measurable performance impact on using the loopback adapter within the VaporProxy class that then sends the HTTP request to the vapor app running on port `8585``.
7+
You can simply work with the code in `App.swift` inside the `src` folder. The configuration is very simple. Once the Lambda starts, the `App()` function is called which you can use to setup Vapor and your routes. Whenver a request is sent to the Lambda function it'll forward it to the Vapor app using the VaporProxy class.
8+
9+
```swift
10+
struct HelloWorld: Content {
11+
let message: String
12+
}
13+
14+
/*
15+
This App() function is called in the Handler when it first
16+
initializes. Routes and any configuration should be done here.
17+
Make sure to retain the App() function or replace it in the Handler.
18+
*/
19+
func App() {
20+
// this is the Vapor app instance from the Vapor Proxy
21+
let app = VaporProxy.shared.app
22+
app.get { req in
23+
return HelloWorld(message: "Hello, world!")
24+
}
25+
}
26+
```
827

928
## Running locally
1029

@@ -17,7 +36,15 @@ sam local start-api --template template.yaml
1736

1837
Note that the performance characteristics of running this application locally using AWS SAM is entirely different from running it on AWS. SAM will use approx 30-40% more memory than the binary will consume with the actual Lambda on AWS. The invocation of SAM will also take more time than it will when actually running on Lambda with API Gateway.
1938

20-
## Sample logs
39+
## Deploying to AWS
40+
41+
You can deploy the application to AWS using either AWS SAM or CloudFormation. With AWS SAM, you can simply use the `sam deploy --guided` command and SAM will guide you through the deployment of the app. If you want to use CloudFormation instead, you need to put the `bootstrap` binary in `bin/` into a zip file and upload it to the S3 bucket that you want to host the code on. The configuration of `AWS::Lambda::Function` is almost identical to `AWS::Serverless::Function`.
42+
43+
## Performance
44+
45+
The vapor integration curently uses the `AsyncHTTPClient` and thus the local loopback on the Lambda instance. Vapor is initialized when the Lambda container first starts making the cold start take around 800-900ms on a 128 MB arm64 container running Amazon Linux 2. There is no measurable performance impact on using the loopback adapter within the VaporProxy class that then sends the HTTP request to the vapor app running on port `8585``.
46+
47+
### Sample logs
2148

2249
The logs give an insight on the performance of the approx. 117 MB binary file within the Lambda. The Lambda cold start with the binary is a little less than 1 second. The log also shows how Vapor runs through the entire lifecycle of the Lambda and is reused for subsequent requests to the same Lambda container. The memory consumption of 39 MB on a 128 MB instance is perfectly in line with web frameworks of Vapor's scale (e.g. Go Gin). The logs do not show any reasonable performance impact of the usage of the loopback adapter in the VaporProxy class.
2350

src/App.swift

+12
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,23 @@
1515
*/
1616
import Vapor
1717

18+
/*
19+
You can do whatever you want in this file, just make sure
20+
to not instanciate the Vapor app, but instead use the instance
21+
from the VaporProxy by using "VaporProxy.shared.app"
22+
*/
23+
1824
struct HelloWorld: Content {
1925
let message: String
2026
}
2127

28+
/*
29+
This App() function is called in the Handler when it first
30+
initializes. Routes and any configuration should be done here.
31+
Make sure to retain the App() function or replace it in the Handler.
32+
*/
2233
func App() {
34+
// this is the Vapor app instance from the Vapor Proxy
2335
let app = VaporProxy.shared.app
2436
app.get { req in
2537
return HelloWorld(message: "Hello, world!")

src/Handler.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ struct APIGatewayProxyLambda: LambdaHandler {
2424
typealias Event = APIGatewayRequest
2525
typealias Output = APIGatewayResponse
2626

27+
/*
28+
This method is called when the Lambda cold starts
29+
and not with every call to the function. If there are
30+
operations that you want to execute once the container
31+
initializes, do it here.
32+
*/
2733
init(context: LambdaInitializationContext) async throws {
28-
print("Serverless Swift cold started!")
2934
App()
3035
VaporProxy.shared.start()
3136
}

src/VaporProxy.swift

+13-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class VaporProxy {
2121
/* singleton instance of the VaporProxy class */
2222
static let shared = VaporProxy()
2323

24+
/* instanciate the app with the class */
2425
var app = Vapor.Application()
2526
let port = 8585
2627
private var running = false
@@ -35,13 +36,22 @@ class VaporProxy {
3536
to the app through this proxy class */
3637
func start() {
3738
guard !running else {
38-
print("Vapor app is already running, not starting it")
39+
// this should never happen as Vapor is started once the
40+
// Lambda initializes. It just ensures it really doesn't
41+
print("Vapor app is already running, not starting it again")
3942
return
4043
}
4144

45+
// bind to the "lo". By using 127.0.0.1 the underlying Linux will
46+
// use its loopback adapter and the request to Vapor will never
47+
// enter any network cards or drivers or even the actual network
48+
// see: https://tldp.org/LDP/nag/node66.html
4249
let address = BindAddress.hostname("127.0.0.1", port: 8585)
4350
app.http.server.configuration.address = address
4451

52+
/* run the Vapor process async as it otherwise would block
53+
the entire Lambda function and it couldn't respond to
54+
any requests */
4555
DispatchQueue.global().async {
4656
do {
4757
self.running = true
@@ -79,9 +89,11 @@ class VaporProxy {
7989
headers.add(name: key, value: value)
8090
}
8191

92+
// Execute the HTTP request against Vapor and exgtract the response
8293
let httpRequest = try HTTPClient.Request(url: url, method: httpMethod, headers: headers, body: body)
8394
let response = try await client.execute(request: httpRequest).get()
8495

96+
// set the body string from the response received from Vapor
8597
let bodyString = response.body!.getString(at: 0, length: response.body!.readableBytes)
8698

8799
var gatewayResponse = APIGatewayResponse(statusCode: .init(code: response.status.code))

0 commit comments

Comments
 (0)