Websites using Swift and AWS Lambda — Part 1

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:

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!


Generating HTML/CSS using Swift

The below source code is available on Github.

Getting the project setup

// 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"]),
    ]
)
  1. The comment swift-tools-version declares the minimum version of Swift required to build this package.
  2. 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.
  3. 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.
  4. 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:

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)
}

  1. Here we import the dependencies that includes the HTML and CSS DSLs.
  2. 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.
  3. This public function generateWebsite will return a String that is HTML. This function will be called from our Lambda function and during local development.
  4. 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.
  5. 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.
  6. 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:

// 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)
}
  1. 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.
  2. We import our internal dependency which generates the HTML.
  3. Get a path for where to put the generated index.html file.
  4. Generate the HTML using the free function from our imported internal dependency.
  5. 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.

To use this for local development:

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:

// 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()
  1. Here we import both the library to interface with AWS Lambda and our internal dependency to generate the HTML/CSS using Swift.
  2. The AWSLambdaSwift project nicely uses the Swift Codable 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 to Codable.
  3. Similar to the Event struct, the AWSLambdaSwift project uses Codable 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.
  4. 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 the GenerateWebsite module, and return the generated HTML.
  5. 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.

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:

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)
  1. 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.
  2. This is the command name to create our Lambda function.
  3. 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 container
    • swift, use the latest Swift Linux Docker image
    • Finally, run swift build using the project name defined in step 1.
  4. Make sure the Artifacts directory exists.
  5. 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:


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!!

Feel free to reach out on Twitter — cheers!

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora