A full-stack serverless application with AssemblyLift and Next.js
Tags: programming, cloud, aws, serverless
Published: 2022-10-11 19:21:45

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](https://github.com/akkoro/assemblylift-demo-nextjs-aws). You can see it live [here](https://nextjs.demos.asml.akkoro.io/)!

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](https://github.com/akkoro/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](https://terraform.io) 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](https://nextjs.org) by [Vercel](https://vercel.com) is a popular JavaScript/TypeScript frontend framework based on [React](http://reactjs.org). 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`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/services/) directory. Each service is made up of a **service manifest** named [`service.toml`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/services/www/service.toml) alongside one or more functions. This demo application deploys one service named [`www`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/services/www/) which contains two functions, [`server`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/services/www/server/) and [`counter`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/services/www/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](https://xata.io) database.  

For performance, the `server` function is written in [Rust](https://rust-lang.org). 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](https://ruby-lang.org). 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](https://aws.amazon.com/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`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/services/www/service.toml#L27):
```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`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/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](https://aws.amazon.com/route53/).

Domain names are defined as an array in [`assemblylift.toml`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/assemblylift.toml#L7):
```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`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/services/www/service.toml#L12):
```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)](https://aws.amazon.com/certificate-manager/).

### 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](https://docs.assemblylift.akkoro.io/learn-assemblylift/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:
```toml
[[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`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/frontend/) directory to mimic the [`services`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/services/) directory. Here we have used `create-next-app` to create a Next.js project called [`www`](https://github.com/akkoro/assemblylift-demo-nextjs-aws/blob/main/frontend/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](https://github.com/akkoro/assemblylift/releases)

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](https://discord.gg/pVSCqYgra3) and follow [@akkorocorp](https://twitter.com/akkorocorp/) on Twitter.