DEV Community

Dan for Akkoro

Posted on • Edited on

Deploy an ultra-fast blog in minutes with Eleventy and AssemblyLift (WebAssembly + Lambda + API Gateway + Rust)

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.

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 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 (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 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 if you do not have one already.

You will also need the Rust toolchain and NPM 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
Enter fullscreen mode Exit fullscreen mode

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:

# Generated with assemblylift-cli

[project]
name = "my-blog"

[services]
default = { name = "my-service" }
Enter fullscreen mode Exit fullscreen mode

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:

# assemblylift.toml
[services]
web = { name = "web" }
Enter fullscreen mode Exit fullscreen mode

We will also need to rename the service's directory:

$ mv services/my-service services/web
Enter fullscreen mode Exit fullscreen mode

Next let's open up the web service manifest, services/web/service.toml:

# Generated with assemblylift-cli

[service]
name = "my-service"

[api.functions.my-function]
name = "my-function"
Enter fullscreen mode Exit fullscreen mode

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:

# web/service.toml
[service]
name = "web"

[api.functions.server]
name = "server"
Enter fullscreen mode Exit fullscreen mode

And rename the function directory:

$ mv services/web/my-function services/web/server
Enter fullscreen mode Exit fullscreen mode

Finally, we'll need to rename the function inside web/server/Cargo.toml:

# server/Cargo.toml
[package]
name = "server"
version = "0.0.0"
.
.
Enter fullscreen mode Exit fullscreen mode

The server function

Let's take another look inside the service manifest, at the api.functions table.

# web/service.toml
[api.functions.server]
name = "server"
Enter fullscreen mode Exit fullscreen mode

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:

# web/service.toml
[api.functions.server]
name = "server"
http = { verb = "GET", path = "/{path+}" }
Enter fullscreen mode Exit fullscreen mode

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:

// 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;
Enter fullscreen mode Exit fullscreen mode

This function uses the 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:

[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" }
Enter fullscreen mode Exit fullscreen mode

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:

$ mkdir www
$ cd www
Enter fullscreen mode Exit fullscreen mode

Create a new package in this directory with npm and then install Eleventy:

$ npm init -y
$ npm install --save-dev @11ty/eleventy
Enter fullscreen mode Exit fullscreen mode

At this point, it will be helpful to take a look at the Eleventy base blog repo 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
Enter fullscreen mode Exit fullscreen mode

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. 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):

$ asml cast
Enter fullscreen mode Exit fullscreen mode

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:

$ asml bind
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

You can then add a new function to the service with:

$ asml make function my-service-name.my-function-name
Enter fullscreen mode Exit fullscreen mode

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

For more details on AssemblyLift, please see the official documentation.

Top comments (0)