A full-stack serverless application with AssemblyLift and Next.js
Dan
Posted on October 11, 2022
Today we published a new demo illustrating several features of the latest AssemblyLift release!
This demo deploys a simple "hello world" Next.js application to an AWS environment. Visits to the app are counted by invoking a logging function from the server function.
The demo repository is on GitHub. You can see it live here!
Topics illustrated:
- Serving static web assets from an AssemblyLift function
- Serving a private API from an AssemblyLift function
- Authoring AssemblyLift functions in both Rust and Ruby
- Sandboxed WebAssembly network access using an IO Module
- Making HTTP calls to another function or to a 3rd party service
- Making calls to an AWS service (Secrets Manager)
- Deploying a service to a custom domain name
- Automated TLS certificate configuration & provisioning
AssemblyLift is an open platform for cloud-native application development. AssemblyLift provides a portable, function-oriented framework and WebAssembly-based runtime which can be deployed to AWS Lambda or Kubernetes. The AssemblyLift CLI generates HashiCorp Terraform infrastructure code from simple TOML definitions, and takes care of compiling and packaging functions and services for deployment. To make a clichéd comparison, think of it as Infrastructure on Rails 😛
Next.js by Vercel is a popular JavaScript/TypeScript frontend framework based on React. Next allows us to export our React application as static HTML which we can serve with AssemblyLift.
Deep dive
Services & Functions
AssemblyLift applications are composed of services, each of which are made up of functions. Services are stored in the services
directory. Each service is made up of a service manifest named service.toml
alongside one or more functions. This demo application deploys one service named www
which contains two functions, server
and counter
, each of which do exactly what it says on the tin :).
The server
function is our HTTP server which serves web content. The counter
function is a private function (protected by IAM) which is called from server
; this function updates a simple count of visits by IP in a Xata database.
For performance, the server
function is written in Rust. Rust compiles natively to WebAssembly, and so has faster cold-start time as well as faster execution speed compared to Ruby -- ideal for our server! We leverage a Rust crate called rust_embed
which allows us to embed assets inside a compiled binary. We use this to embed the assets generated by our Next.js build inside the WebAssembly module which AssemblyLift will deploy as our Lambda function!
The counter
function is written in Ruby. Since Ruby is an interpreted language, AssemblyLift deploys a customized Ruby 3.1 interpreter compiled to WebAssembly, which executes the function handler. Since the interpreter is somewhat large, the cold-start time of a Ruby function tends to be larger than that of a Rust function. Our counter is being run in the backround, so we're fine with it being a little bit laggy at times 😉.
HTTP API
Regardless of infrastructure provider, AssemblyLift services are placed behind some kind of API Gateway service. When deployed to AWS, each AssemblyLift service is placed behind an an Amazon API Gateway endpoint. Each function may define an HTTP route which will invoke it. The server
function is invoked by GET /{path+}
; the {path+}
token is a path parameter where the +
indicates that it is a greedy parameter. This means that everything after the first /
is mapped to a parameter called path
. This allows us to map the requested path to an embedded path in our function binary. The counter
function is invoked by POST /api/counter/{ip}
, where {ip}
is a regular path parameter named ip
.
A function's HTTP route is defined in service.toml
:
[[api.functions]]
name = "counter"
language = "ruby"
size_mb = 3584
http = { verb = "POST", path = "/api/counter/{ip}" } # This defines the HTTP route for counter
authorizer_id = "iam"
The counter
function is protected by API Gateway's built-in IAM authorizer. AssemblyLift doesn't yet support defining access permissions between functions in TOML, so for this demo a small amount of Terraform is included to attach IAM policies to each function. AssemblyLift instantiates the user_tf
directory (if found) as a module alongside its generated module(s).
Domain name mapping
AssemblyLift projects can specify one or more domain names to which services can be mapped using a DNS provider. At the moment, the only available DNS provider is Amazon Route53.
Domain names are defined as an array in assemblylift.toml
:
[[domains]]
dns_name = "demos.asml.akkoro.io"
[domains.provider]
name = "route53"
[domains.provider.options]
aws_region = "us-east-1"
With Route53, the dns_name
must correspond to an existing Route53 Hosted Zone.
By default, services are mapped to subdomains according to the pattern service-name.project-name.domain-name.tld
. For example, the www
service would be www.nextjs.demos.asml.akkoro.io
.
A service can indicate that it is the root service, omitting the service subdomain from the path. For example in service.toml
:
[api]
domain_name = "demos.asml.akkoro.io"
is_root = true
yields the domain nextjs.demos.asml.akkoro.io
for the www
service.
When deployed to AWS, AssemblyLift will provision TLS certificates for your services using Amazon Certificate Manager (ACM).
IO Modules
The counter
function uses a Xata database for storage, which provides a JSON-oriented HTTP API which we can access using the HTTP IO Module. WebAssembly's sandboxing means that by default, functions cannot communicate over a socket or access the filesystem. AssemblyLift provides an RPC interface allowing communication with services called IO Modules or IOmods. IOmods are deployed per-service, i.e. every function in a service share the same set of IOmods. An IOmod is essentially a library of remote functions, each at an assigned coordinate organization.namespace.module.call
.
For example, importing the standard HTTP module:
[[iomod.dependencies]]
coordinates = "akkoro.std.http"
version = "0.3.0"
type = "registry"
exports a single function named request
at akkoro.std.http.request
.
Next Frontend
There is no specific convention for adding a frontend to AssemblyLift; in this project we have created the frontend
directory to mimic the services
directory. Here we have used create-next-app
to create a Next.js project called www
after our service of the same name.
The frontend must be exported to a static site, using next build && next export
. The contents of the generated out
directory are copied into the server binary when it is compiled (on asml cast
).
Deploy your own!
You can deploy this project yourself! You will need to update it to use your own domain of course ;)
You will need...
Required:
- An AWS account
- Rust & Cargo CLI
- Node & NPM CLI
- Latest AssemblyLift CLI
Nice to have:
- A domain name in Route53
An AssemblyLift project is built with the asml cast
command; casting is the process of compiling WASM, transpiling TOML, building images, etc which comprises the build of an application. Artifacts and plans are serialized to the net/
directory.
To deploy a project you use the command asml bind
, which will deploy artifacts and apply the underlying Terraform plan.
Reach out!
Get in touch with us on Discord and follow @akkorocorp on Twitter.
Posted on October 11, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.