A full-stack serverless application with AssemblyLift and Next.js

dotxlem

Dan

Posted on October 11, 2022

A full-stack serverless application with AssemblyLift and Next.js

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

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

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

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

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:

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.

💖 💪 🙅 🚩
dotxlem
Dan

Posted on October 11, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related