Deploy an ultra-fast blog in minutes with Eleventy and AssemblyLift (WebAssembly + Lambda + API Gateway + Rust)
Tags: jamstack, rust, webdev, serverless
Published: 2021-09-28 20:49:55

Hello fellow developers! 😊

Let's talk about static site generators for a minute. Static Site Generators (SSGs) are popular these days for lots of applications, not least of all personal blogs. Static sites offer quick response times owing to easy caching by CDNs. They also have the advantage of not needing access to a database for content. Perhaps best of all, they can often be hosted for free using a simple S3 bucket or a SaaS such as [Netlify](https://netlify.com).

An alternative approach is a serverless backend with AWS Lambda and API Gateway. This would allow our "static" file server to do some processing before serving the final HTML if we like. With Lambda, we can control our "server" performance by adjusting the function's memory/CPU allocation. Using API Gateway allows us to monitor and secure our routes (among other things). Lots of benefits while still being cheap, if not free, and high-performance!

Normally what we've described above would be a pretty complicated set up (and a much longer article 😜). Luckily we can use [AssemblyLift](https://assemblylift.akkoro.io) to do all the heavy work for us!

AssemblyLift is an open platform which allows you to quickly develop high-performance serverless applications. Service functions are written in the Rust programming language and compiled to WebAssembly. The AssemblyLift CLI takes care of compiling and deploying your code, as well as building and deploying all the needed infrastructure!

In this walkthrough we're going to build a blog using [Eleventy](https://11ty.dev) (11ty), a JavaScript SSG. While I was preparing to write this I started putting together my own blog; the source of which you can [take a look at on GitHub](https://github.com/dotxlem/xlem-blog) if you'd like to see approximately what we'll be building (your's will undoubtedly be prettier than mine).

## Preparation & assumed knowledge
To follow this guide you will need an [AWS account](https://portal.aws.amazon.com/billing/signup#/start) if you do not have one already.

You will also need the [Rust toolchain](https://rustup.rs/) and [NPM](https://nodejs.org) installed. The Rust toolchain is installed using the `rustup` interactive installer. The default options during installation should be fine. After installation 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.

### Assumed knowledge
AssemblyLift functions are written in Rust, so it will be helpful to have a working knowledge of Rust. That said if you are only interested in getting your site deployed, you can safely smile and nod at the Rust code and skip ahead 🙃. Eleventy is a JavaScript framework, however the only JS you *need* is its configuration in `.eleventy.js`.

## Project setup
In your favourite *projects* directory, create a new AssemblyLift application for your blog and change to that directory.

```
$ asml init -n my-blog
$ cd my-blog
```

Before we can configure our Eleventy front-end, we'll need to set up our AssemblyLift backend and create our web service.

### Configure the default project
With `asml init` a new AssemblyLift project is generated with the bare minimum needed to build and deploy an application; a single service containing a single function.

First let's take a look at the *application manifest*, `assemblylift.toml`:
```toml
# Generated with assemblylift-cli

[project]
name = "my-blog"

[services]
default = { name = "my-service" }
```

We need a service which will serve our site assets, so we can start by renaming the default service. Update the *services* table and name the service something relevant:
```toml
# assemblylift.toml
[services]
web = { name = "web" }
```

We will also need to rename the service's directory:
```bash
$ mv services/my-service services/web
```

Next let's open up the `web` service manifest, `services/web/service.toml`:
```toml
# Generated with assemblylift-cli

[service]
name = "my-service"

[api.functions.my-function]
name = "my-function"
```

Rename the service to `web`. Then as with our service in the project manifest, we should rename the default function in the service manifest to something relevant:
```toml
# web/service.toml
[service]
name = "web"

[api.functions.server]
name = "server"
```

And rename the function directory:
```bash
$ mv services/web/my-function services/web/server
```

Finally, we'll need to rename the function inside `web/server/Cargo.toml`:
```toml
# server/Cargo.toml
[package]
name = "server"
version = "0.0.0"
.
.
```

## The server function
Let's take another look inside the service manifest, at the *api.functions* table.
```toml
# web/service.toml
[api.functions.server]
name = "server"
```

Pretty sparse right? As is, this will deploy a lambda function without any sort of API (though it can still be invoked like any other function via the AWS SDK!). Let's add an HTTP route to our function so we can invoke it from our browser:
```toml
# web/service.toml
[api.functions.server]
name = "server"
http = { verb = "GET", path = "/{path+}" }
```

The `http` block has a `verb` and a `path`. The verb is the HTTP method the function is invoked with (e.g. GET, POST, PUT, etc.). The path (or "route" in some frameworks) is the HTTP path which is mapped to our function. In this case we are using a feature of API Gateway called a *proxy path*. This is the `{path+}` variable, which globs the entire path as a single variable called `path` inside our function code.

### The function code
So far we've got our infrastructure defined, but the default function code doesn't do much of anything.

Open up `web/server/src/lib.rs`, and overwrite it with the following:
```rust
// lib.rs
// www/server

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.ends_with("/") {
        true => format!("{}index.html", &path[1..path.len()]),
        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 = "../../../www/_site"]
struct PublicAssets;
```

This function uses the [`rust-embed`](https://crates.io/crates/rust-embed) crate, which embeds the contents of a directory inside the compiled binary. We use this to store the site generated by Eleventy inside the function!

We can then use the `path` variable passed to the function via the function input (remember our path parameter, `{path+}`?) to select which resource to return as our function output. A `match` block is used to detect if we have a path ending in a forward slash, indicating that we should default to serving `index.html` from that path.

Other crates are imported as well, to handle Gzip & Base 64 encoding and as well as guessing MIME types for our response headers. In all, you should have the following dependencies in your `Cargo.toml`:
```toml
[dependencies]
base64 = "0.13"
direct-executor = "0.3.0"
flate2 = "1"
mime_guess = "2"
rust-embed = "6"
serde = "1"
serde_json = "1"
asml_core = { version = "0.2", package = "assemblylift-core-guest" }
assemblylift_core_io_guest = { version = "0.3", package = "assemblylift-core-io-guest" }
asml_awslambda = { version = "0.3", package = "assemblylift-awslambda-guest" }
```

## Installing Eleventy
As of now our base infrastructure is ready, but compiling our function will fail because our `www` directory doesn't exist yet! Let's fix that; first by creating the directory in our project root:
```bash
$ mkdir www
$ cd www
```

Create a new package in this directory with `npm` and then install Eleventy:
```bash
$ npm init -y
$ npm install --save-dev @11ty/eleventy
```

At this point, it will be helpful to take a look at the [Eleventy base blog repo](https://github.com/11ty/eleventy-base-blog) on GitHub. I recommend copying `.eleventy.js` into `www` and installing these additional packages:
```
npm install --save-dev @11ty/eleventy-navigation @11ty/eleventy-plugin-rss @11ty/eleventy-plugin-syntaxhighlight luxon markdown-it markdown-it-anchor
```

From there the structure of your site is really up to you! Look at examples and determine what you like best. 

Running `npx @11ty/eleventy` will generate the static site from any template files it finds in the current directory, and by default write the output to `_site/`. You will at least need `index.*` in order to generate an `index.html` to serve.

A powerful feature of Eleventy is [collections](https://www.11ty.dev/docs/collections/). Pages with the same tag are grouped into a collection by the same name, and made available inside our templates. In the base blog repo for example, all posts are placed in a directory and a single JSON file specifies the tags assigned to everything in the directory. Now adding a post to your blog is as easy as adding a new file to your `posts` directory!

## Build & deploy our new site
Once you have something which builds without error in `www/_site`, you should have everything necessary to build our application and deploy!

AssemblyLift applications are built with the `cast` command, inside the project root (the directory where `assemblylift.toml` is found):
```bash
$ asml cast
```

This will compile our web service & server function, and generate a Terraform infrastructure plan. All build artifacts are serialized in the `net/` directory. When our Rust function is compiled, our static site assets are bundled with the resulting WebAssembly binary!

To deploy our new service, simply run:
```bash
$ asml bind
```

### Testing the web service
If everything in the `bind` command completed without error, you should now be able to find a new API Gateway endpoint inside the AWS console. The API will be named `asml-{projectName}-{serviceName}`. Navigate to your API details, where you should find an endpoint listed for the `$default` stage. Opening this URL in a browser should render your new statically generated blog!

## Further enhancements
For production use, you will probably want to create a CloudFront distribution using your web server service as the distribution source. The API Gateway service also supports custom domain names, as the generated APIGW/CloudFront URLs are quite ugly 😁.

If in the future you want to expand your blog with dynamic content, you can create additional services & functions. Note however that if you intend on using CloudFront or other CDN, the `web` service should **only** contain the `server` function and all other functionality should reside in other services. This is because the server function uses a proxy path; no other functions in the service would receive invocations!

In AssemblyLift a new service is created by running:
```
$ asml make service my-service-name
```

You can then add a new function to the service with:
```
$ asml make function my-service-name.my-function-name
```

Any new functions you make which you don't want to be publicly accessible should attach an authorizer. Take a look at [this post on using Auth0 with AssemblyLift](https://dev.to/akkoro/lambda-function-http-authorization-with-auth0-and-assemblylift-webassembly-lambda-api-gateway-rust-4fl8) for an in-depth guide!

## That's all, folks!
If you have any questions, don't hesitate to reach out in the comments below or [on GitHub](https://github.com/akkoro/assemblylift/discussions)!

For more details on AssemblyLift, please see the [official documentation](https://docs.assemblylift.akkoro.io).