Deploy an ultra-fast blog in minutes with Eleventy and AssemblyLift (WebAssembly + Lambda + API Gateway + Rust)
Dan
Posted on September 28, 2021
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
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" }
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" }
We will also need to rename the service's directory:
$ mv services/my-service services/web
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"
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"
And rename the function directory:
$ mv services/web/my-function services/web/server
Finally, we'll need to rename the function inside web/server/Cargo.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.
# 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:
# 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:
// 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
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" }
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
Create a new package in this directory with npm
and then install Eleventy:
$ 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 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. 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
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
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 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.
Posted on September 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 28, 2021