Deploy a Jamstack site on AWS Lambda with API Gateway in 10 minutes or less 💨
Tags: jamstack, aws, rust, tutorial
Published: 2021-08-12 16:48:53

[Jamstack](https://jamstack.org) 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](https://aws.amazon.com/lambda/) and [API Gateway](https://aws.amazon.com/api-gateway/). To accomplish this we will use [AssemblyLift](https://assemblylift.akkoro.io), an open-source platform designed to quickly & easily accomplish such a task.

## Getting started
You will need an [AWS account](https://portal.aws.amazon.com/billing/signup#/start) 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](https://rustup.rs/) and [NPM](https://nodejs.org) 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](https://github.com/akkoro/assemblylift-template-jamstack). 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
```

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
```rust
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
```

### How does it work?
Most of the heavy lifting in this function is done by [`rust-embed`](https://crates.io/crates/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`:
```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" }
```
You can learn more about the AssemblyLift TOML documents on the [official docs](https://docs.assemblylift.akkoro.io/learn-assemblylift/services), 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rq37i0iwt2dkwetcih5q.png)

Click on the listed API to view more details.
![A screenshot of the API Gateway console showing the listed API stages](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2fczwzdaavs8sdtu2ow7.png)
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`](https://crates.io/crates/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](https://docs.assemblylift.akkoro.io).

You can also reach out to us on [Twitter](https://twitter.com/akkorocorp), via [Element](https://element.io) at #assemblylift:matrix.org, or in the comments below!