Deploy a Jamstack site on AWS Lambda with API Gateway in 10 minutes or less šŸ’Ø

dotxlem

Dan

Posted on August 12, 2021

Deploy a Jamstack site on AWS Lambda with API Gateway in 10 minutes or less šŸ’Ø

Jamstack architecture is all the rage these days, owing to its focus on high performance and scalability. With an emphasis on serving pre-generated content, for some it may feel a little bit like a return to the old days. The underlying infrastructure however is anything but!

This article will walk through building and deploying a simple static site, served using AWS Lambda and API Gateway. To accomplish this we will use AssemblyLift, an open-source platform designed to quickly & easily accomplish such a task.

Getting started

You will need an AWS account if you do not have one already. The small application you will build here fits comfortably in the free tier. šŸ™‚

You will also need the Rust toolchain and NPM installed. In addition you will need to install the wasm32-unknown-unknown build target for the Rust toolchain (rustup toolchain install wasm32-unknown-unknown).

Once you have these prerequisites you can install AssemblyLift with cargo install assemblylift-cli. Run asml help to verify the installation.

Creating a Jamstack project

We will base our application on the AssemblyLift project template available on GitHub. You can clone this, or click the "use this template" button to create a new repo in your account from the template.

The template project includes a simple static site that we will build with Webpack. It also includes an AssemblyLift service called www which implements our server function.

Project structure

Let's take a closer look at the project.

ā”œā”€ā”€ README.md
ā”œā”€ā”€ assemblylift.toml
ā”œā”€ā”€ babel.config.json
ā”œā”€ā”€ package-lock.json
ā”œā”€ā”€ package.json
ā”œā”€ā”€ services
ā”‚Ā Ā  ā””ā”€ā”€ www
ā”‚Ā Ā      ā”œā”€ā”€ server
ā”‚Ā Ā      ā”‚Ā Ā  ā”œā”€ā”€ Cargo.lock
ā”‚Ā Ā      ā”‚Ā Ā  ā”œā”€ā”€ Cargo.toml
ā”‚Ā Ā      ā”‚Ā Ā  ā””ā”€ā”€ src
ā”‚Ā Ā      ā”‚Ā Ā      ā””ā”€ā”€ lib.rs
ā”‚Ā Ā      ā””ā”€ā”€ service.toml
ā”œā”€ā”€ web
ā”‚Ā Ā  ā”œā”€ā”€ images
ā”‚Ā Ā  ā”‚Ā Ā  ā””ā”€ā”€ AssemblyLift_logo_with_text.png
ā”‚Ā Ā  ā”œā”€ā”€ main.js
ā”‚Ā Ā  ā”œā”€ā”€ style
ā”‚Ā Ā  ā”‚Ā Ā  ā””ā”€ā”€ main.css
ā”‚Ā Ā  ā””ā”€ā”€ views
ā”‚Ā Ā      ā””ā”€ā”€ index.ejs
ā””ā”€ā”€ webpack.config.js
Enter fullscreen mode Exit fullscreen mode

The project root contains configuration files for AssemblyLift, Babel, NPM, and Webpack. There is also the AssemblyLift services directory, and a directory called web which we've chosen for our frontend code. Feel free to rename the web directory if you like, just don't forget to update the Webpack config accordingly!

This project has one service www containing one function server (sometimes written www/server or www.server). The server function is a Rust crate containing the function handler.

A closer look

Next let's look at the handler function in www/server. You won't need to change it for this walkthrough, but it will be helpful to understand how it works if you want to expand on it yourself šŸ™‚.

The code

extern crate asml_awslambda;

use std::io::Write;

use base64::encode;
use flate2::write::GzEncoder;
use flate2::Compression;
use mime_guess;
use rust_embed::RustEmbed;

use asml_awslambda::*;
use asml_core::GuestCore;

handler!(context: LambdaContext<ApiGatewayEvent>, async {
    let path = context.event.path;
    let path = match path == "/" {
        true => String::from("index.html"),
        false => String::from(&path[1..path.len()]),
    };

    AwsLambdaClient::console_log(format!("Serving {:?}", path.clone()));

    match PublicAssets::get(&path.clone()) {
        Some(asset) => {
            let mut gzip = GzEncoder::new(Vec::new(), Compression::default());
            let mime = Some(
                mime_guess::from_path(path.clone())
                    .first_or_octet_stream()
                    .as_ref()
                    .to_string(),
            );
            let data = asset.data.as_ref();
            gzip.write_all(data).unwrap();
            let body = encode(gzip.finish().unwrap());
            http_ok!(body, mime, true, true) // true, true: we always gzip & encode base64
        }
        None => http_not_found!(path.clone()),
    }
});

#[derive(RustEmbed)]
#[folder = "../../../dist/"]
struct PublicAssets
Enter fullscreen mode Exit fullscreen mode

How does it work?

Most of the heavy lifting in this function is done by rust-embed, which embeds the contents of dist (our Webpack output directory) in the compiled binary. This allows our static assets to be deployed to Lambda bundled inside the function code, as long as the resulting binary is less than 50MB.

The rest of the function code deals mainly with API Gateway. The path received by the function is matched against the embedded assets; if one is found, it is gzipped and then encoded base64 as required by API Gateway to serve binary content. The content-type header is set using the mime_guess crate, defaulting to application/octet-stream if type cannot be guessed.

Configure, build, deploy

Technically you can deploy the template as-is without any configuration, but you will likely want to at least rename the project. To do this look at assemblylift.toml:

[project]
name = "assemblylift-template-jamstack"

# In production you should configure an S3 bucket & DynamoDB table for terraform state
#[terraform]
#state_bucket_name = "my-tf-state"
#lock_table_name = "my-tf-lock"

[services]
default = { name = "www" }
Enter fullscreen mode Exit fullscreen mode

You can learn more about the AssemblyLift TOML documents on the official docs, however this is where you would set the name of the project (included in the names of the created AWS resources) as well as add more services if you want. There is also a block for configuring Terraform remote state storage, which is highly recommended for use in production!

To build an AssemblyLift application you use the asml cast command; this will compile everything which needs to be deployed and store it immutably in the net directory.

If you haven't built the Webpack project however, you will get an error! Build your web assets first with npm run build. Once they are available in dist, your function code should build no problem.

Finally to deploy our service! AssemblyLift applications are deployed with asml bind. This process delegates to Terraform and will require appropriate AWS credentials to be available.

Testing the server function

Unfortunately you'll have to leave the command line for the next part, as we'll need to access the AWS console to find our new service URL. Ideally in a future release the Asml command line will do more of this for you :).

First navigate to the API Gateway section of the AWS Console. You should see an API by the name you gave above listed here.
A screenshot of the API Gateway console showing a single API listed

Click on the listed API to view more details.
A screenshot of the API Gateway console showing the listed API stages
This is where each API stage is listed; AssemblyLift always uses the default stage. The URL is the endpoint used to invoke your API.

Our API defines only one route, /{path+} which is called a proxy path. This instructs API Gateway to take the entire path as a variable called path. Our server function will map the path / to /index.html.

If you click on the URL, you should get a very simple static site with the AssemblyLift logo returned back to you!

Next steps

The first thing you'll want to do of course is replace the template site with your own. You can start by just replacing the content, but you're not obligated to keep using Webpack! This function should be compatible with any framework or site generator that ends with a directory of static assets.

If you paid attention to the function code above, it might have occurred to you that the assets we serve don't have to be completely static. For example we could instead embed a handlebars template or similar & render it with the handlebars crate, allowing us "semi-dynamic" content for lack of a better name.

You may also want to create a CloudFront distribution for your API. I have found performance adequate without a CDN, however it will likely improve with one. Putting a CDN in front of the function should also help hide or smooth out latency due to cold start time, if the function is not invoked regularly. CloudFront is not expensive, however whether it is worth it will be up to you šŸ™‚.

Learn More

The best way to learn about AssemblyLift right now is by visiting the official documentation.

You can also reach out to us on Twitter, via Element at #assemblylift:matrix.org, or in the comments below!

šŸ’– šŸ’Ŗ šŸ™… šŸš©
dotxlem
Dan

Posted on August 12, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related