Websites using Swift and AWS Lambda — Part 1
Summary
Want to see this kind of Swift website in action!? Check out the example website, and here is the Source code!
Ever since the announcement of the Swift language, I have been deeply interested in expanding my use of the language to platforms other than the Apple ecosystem (iOS apps, macOS apps, etc.). This interest combined with taking on more web development at work got me thinking — Can I make lightweight websites written in Swift that are simple, fun to write, and easy deploy? The result:
- A website written in Swift using HTML and CSS domain-specific languages (DSLs), which comes with all the benefits of using the Swift language such as writing safer code that is easy to understand and fun to write.
- ~$0 to host!**
- Hosted on Amazon Web Services (AWS) using Lambda, Layers, API Gateway, and optionally Certificate Manager.
- **At the time of this writing, AWS Lambda give you 1 million requests for free per month, and API Gateway costs $3.50 per million requests per month. I would be surprised if hosting a website this way costs you much, if anything at all.
- Simple to deploy and update. All you need to do is update your AWS Lambda function!
- Oh, and the breakdown below will get you set up with a way to do local development.
- A website that is dynamically generated, which means you can put your site together after doing things like querying a database, calling into another Lambda function, or making an API request.
- None of the traditional web JavaScript dependencies to manage, which can be quite overwhelming at times.
- As far as I know, the example website is the first of its kind in that it uses Swift and AWS Lambda 🤓.
Due to the large number of things to discuss, this project will be broken down into two parts: Part 1 — Making the Swift website, and Part 2 — Hosting the website.
let’s work through the first part!
The Tech Stack
This may look like a lot, but don’t worry. I will break everything down!
-
Swift-Html — A Swift domain specific language (DSL) for writing HTML. This package lets you write your HTML in Swift! Because of this, you get all the power of the Swift type system. This means that the HTML spec is codified to prevent you from writing invalid HTML such as putting a paragraph element in an ordered list!
-
Swift-Css — Similar to the Swift-Html package above, but for cascading style sheets (CSS). Other modules in this package are available for use, but they aren’t used in this project. I would think that splitting out the CSS part of this package into its own package like the Swift-Html package is on the to do list. Until then, ignore the other modules.
Note 1: If you are unfamiliar with HTML and CSS but know iOS development, think of HTML as the UI building blocks (UIViews, UIButtons, etc.) that you can drag around in Interface Builder. Think of CSS as autolayout combined with all the UI polish capabilities like setting background color and text font.
Note 2: The previous two packages, are from the folks at Point-Free. Their amazing educational site is about teaching functional concepts using the Swift programming language. One neat thing about their approach to education is that they start from core principles to build up to more complicated topics that they use themselves for their site — dogfooding at its finest 🐶!
Swift-Html and Swift-Web are great examples of being built up piece-by-piece from scratch in their videos, all while the end result is used to generate the site that you are learning off!
-
AWS Lambda — This is an AWS’s solution for serverless computing. You write some code that sits in the cloud and gets called to run. For this project, we will route URL requests to a Lambda that is setup to generate HTML/CSS in Swift to produce a website. One nice thing, is that you are not paying for a server to always be on. When called, the Lambda wakes up runs your code and goes away, adapting to the volume of requests coming in. In most cases, you end up paying close to $0! Part 2 of this series will include more details about pricing.
Note: I use the terms AWS Lambda and Lambda interchangeably.
-
AWS Layers — This is a new offering from AWS to allow you to create your own runtime for Lambdas. In this case, we make a Swift runtime to allow our Swift code to execute when the Lambda is invoked. Before AWS Layers, AWS Lambda functions needed to be written in an AWS Lambda supported language such as Python and Go.
- AWS-Lambda-Swift — This project provides two things:
- Makes a Swift AWS Layer runtime for AWS Lambda as mentioned above (more on this in Part 2).
- Bridges communication between your Swift code and AWS Lambda.
- Let’s you register a function that will get called when your Lambda function gets called.
- Handles passing information to you Swift function when your Lambda function gets called.
- Facilitates getting the response from your Swift function and passing it back to Lambda for further processing. In this case, handles passing back the generated HTML string.
-
API Gateway — We use this to map a domain name URL that triggers the Lambda function. In this case, we are routing a URL request of https://swift-aws-lambda-website.jasonzurita.com to trigger a Lambda function that then returns a website!
- AWS Certificate Manager — If you want to have a custom domain name for your website, you will need to establish that you are the owner of that domain for later use in API Gateway. Among other things, AWS Certificate Manager allows you verify domain ownership and therefore have a certificate for your domain. This will give your URL https, which denotes that your website/domain is secure. Nothing too much to pay attention to with this other than this is a good thing and is a way to get a custom domain name.
- Swift Package Manager — The Swift Package Manager (SPM) is the de facto way to manage Swift dependencies, build, test, and run Swift projects. To learn more about the ins and outs of the SPM, check out the above link and the below resources:
- Example Usage — great way to get started
- GitHub reference — useful to learn about the package description API
- Product definitions — Learn more about SPM products and why they were introduced
-
Make — Used to simplify building our Swift Lambda function.
-
Docker — Docker may seem out of place here, but AWS Lambda functions execute in a Linux environment. If we were to just build our Lambda on a Mac and upload it, it wouldn’t run because that Swift executable wasn’t made to run on Linux. We use Docker to spin up a Linux environment where we build our Swift executable to target Linux, and therefore will run in AWS Lambda. The more I learn about Docker, the more useful I find it!
- Domain name hosting (such as GoDaddy) — If you want a custom URL like what I have for this blog or the example Swift website, you will need to buy it. I use GoDaddy, but there are others out there including an AWS offering called Amazon Route 53. If I was starting from scratch, I would probably go with Route 53, but I have been using GoDaddy for a while now and I am used to using it.
Generating HTML/CSS using Swift
The below source code is available on Github.
Getting the project setup
- Open up your terminal.
- Make and switch to a directory for your Swift website project (the desktop is used as an example below).
mkdir ~/Desktop/Swift-AWS-Lambda-Website
cd !$
- Start a new Swift project.
swift package init
-
This will give you a new Swift project.
- Feel free to delete the
Tests
directory and everything inside of theSources/Swift-AWS-Lambda-Website
directory. We don’t need them for this project.
- Open your Package.swift for editing.
- I use vim, but you can use any text editor of your choosing. If you want a little more insight into why I use vim, I break it down in this blog post.
- Edit the Package.swift file to match the following:
- Check out the resources under the SPM section above to learn more about the format of this file.
// swift-tools-version:4.2
// 1
import PackageDescription
let package = Package(
name: "Swift-AWS-Lambda-Website",
// 2
products: [
.executable(name: "Swift-AWS-Lambda-Website", targets: ["Swift-AWS-Lambda-Website"]),
.executable(name: "Local-Website", targets: ["Local-Website"]),
],
// 3
dependencies: [
.package(url: "https://github.com/tonisuter/aws-lambda-swift.git", .branch("master")),
.package(url: "https://github.com/pointfreeco/swift-html.git", .exact("0.2.1")),
.package(url: "https://github.com/pointfreeco/swift-web.git", .revision("2c3d440")),
],
// 4
targets: [
// 4a
.target(
name: "GenerateWebsite",
// Note:
// Html below comes from swift-html above & Css and HtmlCssSupport come from swift-web above
dependencies: ["Html", "Css", "HtmlCssSupport"]),
// 4b
.target(
name: "Local-Website",
dependencies: ["GenerateWebsite"]),
// 4c
.target(
name: "Swift-AWS-Lambda-Website",
dependencies: ["AWSLambdaSwift", "GenerateWebsite"]),
]
)
- The comment
swift-tools-version
declares the minimum version of Swift required to build this package. - Products allow you to explicitly define the artifacts from your Swift project. This is where you define what your project produces such as a libraries or executables. In this case, we are making a product to generate an executable to upload as the Lambda function and a product to generate an index.html file that we can open and use for local development.
- Explicit products are a more recent addition to the Swift Package manager. To read up on their addition, check out the Swift Evolution proposal: Package Manager Product Definitions.
- This is where you can define other Swift library projects as dependencies to use in your project. The URL needs to resolve to a git repo that has a
Package.swift
in its root directory. You can also specify the version of the dependency that you want to use. - Here we define the different targets that this Swift project produces. Targets may be used to produce products, as noted above, or as internal dependencies.
- a. This target is an internal dependency that is the heart of this project. This is where we write our website. The reason this is broken out into a separate target is that we wanted to be able to use this twice: once for making the AWS Lambda executable, and once to write the generated HTML/CSS to a file for local development.
- b. This target will be used for local development by making a local
index.html
file so we can simplify working on the website. That is, we don’t need to upload a compilied Lambda function to AWS Lambda each time we want to see what a change does to the website! - c. This target is used to make the Swift Lambda executable, which will be uploaded to AWS Lambda to handle incoming requests to produce the website.
The website
We have a Package.swift, but we still need to implement the three different targets. Let’s do that! Starting with the internal dependency — the GenerateWebsite target.
Do the following:
- Change to the sources directory.
cd ~/Desktop/Swift-AWS-Lambda-Website/Sources
- Make and switch to a new source directory.
mkdir GenerateWebsite
cd !$
- Make a file named GenerateWebsite.swift with the below code.
- Copy the below code, then
pbpaste > GenerateWebsite.swift
Note: if you haven’t already, check out the website in action: https://swift-aws-lambda-website.jasonzurita.com
// 1
import Foundation
import Html
import Css
import HtmlCssSupport
import Prelude
// 2
public func zIndex(_ index: Int) -> Stylesheet {
return key("z-index", "\(index)")
}
// 3
public func generateWebsite() -> String {
// 4
let bodyStyle = height("100%")
<> margin(all: 0)
<> backgroundColor(.rgb(0xCC, 0xCC, 0xCC))
let flexContainer = height("100%")
<> margin(all: 0)
<> padding(all: 0)
<> display(.flex)
<> align(items: .center)
<> fontFamily(["Arial", "Helvetica", "sans-serif"])
<> justify(content: .center)
<> fontSize(.rem(1.5))
<> color(.white)
let imageStyle = position(.absolute)
<> top(0)
<> left(0)
<> right(0)
<> bottom(0)
<> margin(all: .auto)
<> zIndex(-1)
let centerRow = width(.auto)
let item = height(.auto)
<> width(.auto)
<> textAlign(.center)
<> color(.rgb(0x33, 0x33, 0x33))
let hrStyle = position(.relative)
<> width("40%")
// 5
let document = html([
body([style(bodyStyle)], [
// 5a
img([src("{logo URL omitted for readability}"), alt(""), style(imageStyle)]),
div([style(flexContainer)], [
div([style(centerRow)], [
div([style(item)], [
h1(["Welcome!"]),
h3(["A demo website written in Swift & hosted using AWS Lambda"]),
hr([style(hrStyle)]),
]),
div([style(item)], [
p([ "Check out the related ",
a([href("https://www.jasonzurita.com/websites-using-swift-and-aws-lambda/")], ["blog post."]),
]),
]),
]),
]),
]),
])
// 6
return render(document)
}
- Here we import the dependencies that includes the HTML and CSS DSLs.
- This free function isn’t included in the CSS dependency yet, so we simply define it here for later use when placing the logo image behind the text on the website.
- This public function generateWebsite will return a String that is HTML. This function will be called from our Lambda function and during local development.
- I am not going to get into the specifics of the CSS here, but this CSS is used to style the website, much like other websites.
- Same thing about the HTML DSL here, which is stored as a local constant named document. Just know that this uses the Html library and the HtmlCssSupport to connect the above CSS to the various HTML elements.
- 5a. The logo image URL was omitted for clarity here, but you can use any image you want here.
- Finally, all we need to do is take the document created in step 5 above, call render on it, and return that HTML string — and there is your website 🎊!
Local development
This is great, but what good is the ability to generate HTML/CSS if we don’t have a way to view it? The following target breakdown, Local-Website, is for local development. That is, a way to speed up development of your website before deploying it to Lambda, which would take longer to see your changes. Having this will save time during development! This target is one of the two products of our Swift project.
Do the following:
- Change to the sources directory.
cd ~/Desktop/Swift-AWS-Lambda-Website/Sources
- Make and switch to a new source directory.
mkdir Local-Website
cd !$
- Make a file named main.swift with the below code.
- Copy the below code, then
pbpaste > main.swift
// 1
import Foundation
// 2
import GenerateWebsite
// 3
let artifactsDirectory = FileManager.default.currentDirectoryPath + "/Artifacts"
let outputFilePath = artifactsDirectory + "/index.html"
// 4
let html = generateWebsite()
// 5
guard let fileHandle = FileHandle(forWritingAtPath: outputFilePath) else {
try? FileManager.default.createDirectory(atPath: artifactsDirectory,
withIntermediateDirectories: false,
attributes: nil)
try? html.write(toFile: outputFilePath, atomically: true, encoding: .utf8)
exit(1)
}
if let data = html.data(using: .utf8) {
fileHandle.write(data)
}
- We need to use Foundation to write out to a file. The main thing to note here is that the imported Foundation here is different depending on if this is run on macOS or Linux. Not an issue in this case, but is something to consider if you want to test on macOS but your production environment for your Swift project is on Linux.
- We import our internal dependency which generates the HTML.
- Get a path for where to put the generated
index.html
file. - Generate the HTML using the free function from our imported internal dependency.
- The remaining lines simply overwrite or create the
index.html
file with the newly generated HTML.- The output file will be located in the directory named Artifacts, which is automatically created as part of creating the
index.html
file here. - If you want to learn more about writing to a file, check out my previous post called Swifty File Reading and Writing.
- The output file will be located in the directory named Artifacts, which is automatically created as part of creating the
To use this for local development:
- Go to your root project directory. In this case:
cd ~/Desktop/Swift-AWS-Lambda-Website
- Run this in your terminal.
swift run Local-Website
- Open the generated index.html file in your web browser of choice, located in the Artifacts directory.
Note: If you try and run the above local flow, you will get an error because we defined the Swift-AWS-Lambda-Website target in our Package.swift, but have not implemented it yet. Do the following step, and come back to running the local flow after.
Make the Lambda function to upload to AWS
Last but not least, let’s breakdown the Swift-AWS-Lambda-Website
target.
Do the following:
- Change to the auto-generated Swift-AWS-Lambda-Website directory.
cd ~/Desktop/Swift-AWS-Lambda-Website/Sources/Swift-AWS-Lambda-Website
- this directory should have ready been created for you when initially running
swift package init
- this directory should have ready been created for you when initially running
- If you haven’t already, delete the auto-generated Swift_AWS_Lambda_Website.swift to have a blank start.
rm Swift_AWS_Lambda_Website.swift
- Make a file named main.swift with the below code.
- Copy the below code, then
pbpaste > main.swift
// 1
import AWSLambdaSwift
import GenerateWebsite
// 2
struct Event: Codable { }
// 3
struct Result: Codable {
let html: String
}
// 4
func handler(event: Event, context: Context) -> Result {
return Result(html: generateWebsite())
}
// 5
let runtime = try Runtime()
runtime.registerLambda("handler", handlerFunction: handler)
try runtime.start()
- Here we import both the library to interface with AWS Lambda and our internal dependency to generate the HTML/CSS using Swift.
- The
AWSLambdaSwift
project nicely uses the SwiftCodable
protocol to pull out information when the lambda function gets called. In this case, we don’t need any of that information, so we make an empty struct that conforms toCodable
. - Similar to the
Event
struct, theAWSLambdaSwift
project usesCodable
as a vehicle for the response from the Lambda. The return from this Lambda is a struct with one property — a string that is HTML. We will pull the HTML out for the final response in API Gateway. More on that later in Part 2. - This is the Swift function that gets registered as the AWS Lambda handler. The implementation of this function is simple — call the
generateWebsite
free function from theGenerateWebsite
module, and return the generated HTML. - Finally, we create the runtime, register the handler with the runtime, and start the runtime.
- Note: the handler in AWS Lambda will need to to be set to
Swift-AWS-Lambda-Website.handler
. More on this in Part 2.
- Note: the handler in AWS Lambda will need to to be set to
We can run this product like the local development product, but as mentioned before the executable will not be able to run in AWS Lambda since it will be targeting macOS. The executable needs to be built targeting Linux. This is where Make and Docker come in.
Do the following:
- Change to the root project directory.
cd ~/Desktop/Swift-AWS-Lambda-Website
- Create a Makefile with the below code.
- Copy the below code, then
pbpaste > Makefile
Note: you will need Make and Docker installed
# 1
PROJECT=Swift-AWS-Lambda-Website
# AWS Lambda needs a handler executable to be:
# - Built to run on Linux (Docker is used for this!)
# - Zipped up
# 2
build_lambda:
# 3
docker run \
--rm \
--volume "$(shell pwd)/:/src/$(PROJECT)" \
--workdir "/src/$(PROJECT)" \
swift \
swift build --product $(PROJECT)
# 4
mkdir -p Artifacts
# 5
zip -r -j Artifacts/lambda.zip $(shell pwd)/.build/debug/$(PROJECT)
- This is a constant that is the name of the product that will be run to make the AWS Lambda executable. This can be changed if your product’s name is different.
- This is the command name to create our Lambda function.
- As mentioned, we use Docker to build the Lambda function while targeting Linux.
--rm
, automatically remove the container when running finishes--volume
, mount the project on your computer to a location in the Docker container, so Docker knows what source files to build--workdir
, working directory inside the containerswift
, use the latest Swift Linux Docker image- Finally, run
swift build
using the project name defined in step 1.
- Make sure the Artifacts directory exists.
- AWS Lambda requires that we zip up the executable for upload, so that is what we do here. The output of this step, named lambda.zip, is placed into the same Artifacts directory as used above for local development.
To generate the Lambda function zip:
- Go to your root project directory. In this case:
cd ~/Desktop/Swift-AWS-Lambda-Website
- Run this in your terminal.
make build_lambda
- or, simply
make
- or, simply
- The output zip will be located in the Artifacts directory!
Now, we are all set up to make this website live 🎉! In Part 2, we will work through just that — deployment & hosting using AWS! This will include some notes about pricing, setting up the Swift runtime using AWS Lambda Layers, the Lambda function, and creating a custom URL using API Gateway and Certificate Manager.
Summary
Note: While waiting for Part 2, try experimenting with making your own website using local development!
We accomplished a lot!!
- We touched on the projects that help us create our website using Swift.
- We setup fast iterative local website development.
- We can generate the zipped up website executable that we will use to upload to AWS Lambda. To do this, we leveraged Docker and Make to simplifying building against a Linux environment.
Feel free to reach out on Twitter — cheers!