Developing a NextJS app on OpenFaaS
Tags: javascript, react, serverless, kubernetes
Published: 2021-11-10 19:26:03

## Prerequisites

The instructions covered in this article require the following tools to be installed before proceeding.

- faas-cli - https://github.com/openfaas/faas-cli
- Terraform - https://terraform.io
- kubectl - https://kubernetes.io/docs/tasks/tools/
- Docker - https://www.docker.com/get-started
- Helm - https://helm.sh

Personally, I prefer to leverage [VS Code Remote Containers](https://code.visualstudio.com/docs/remote/containers) to create portable development environments. Below you'll find the `devcontainer.json` and the `Dockerfile` to put inside your project's `.devcontainer` folder.

```json
{
"name": "<appname>",
	"build": {
		"dockerfile": "Dockerfile",
		// Update 'VARIANT' to pick an Alpine version: 3.11, 3.12, 3.13, 3.14
		"args": {
			"VARIANT": "3.14",
			"DOCKER_GID": "1001",
			"NODE_VERSION": "14"
		}
	},
	
	// Set *default* container specific settings.json values on container create. 
	"settings": {},
	"mounts": [
		"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind",
		"source=${env:HOME}${env:USERPROFILE}/.kube,target=/home/vscode/.kube,type=bind"
	],

	// Add the IDs of extensions you want installed when the container is created.
	// Note that some extensions may not work in Alpine Linux. See https://aka.ms/vscode-remote/linux.
	"extensions": [
		"ms-kubernetes-tools.vscode-kubernetes-tools",
		"ms-azuretools.vscode-docker"
	],

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// "forwardPorts": [],

	// Use 'postCreateCommand' to run commands after the container is created.
	// "postCreateCommand": "uname -a",

	// Replace when using a ptrace-based debugger like C++, Go, and Rust
	// "runArgs": [ "--init", "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ],
	"runArgs": ["--init"],

	// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
	"remoteUser": "vscode"
}
```

{% gist https://gist.github.com/mikeyGlitz/437cf558e537883fe7b47761cb83eb71 %}

## Creating the OpenFaaS Deployment

The first step in deploying an application to OpenFaas is to deploy the OpenFaaS platform to Kubernetes. I use [Helm](helm.sh) and [Terraform](terraform.io) to create the OpenFaaS deployment.
OpenFaaS provides a [helm chart](https://github.com/openfaas/faas-netes/tree/master/chart/openfaas)

```tf
provider "kubernetes" {
  config_context = "docker-desktop"
  config_path = "~/.kube/config"
}
provider "helm" {
  kubernetes {
    config_context = "docker-desktop"
    config_path = "~/.kube/config"
  }
}

variable "openfaas_password" {
  type = string
  description = "OpenFaaS admin password"
}

resource "kubernetes_namespace" "ns_openfaas_fn" {
  metadata {
    name = "openfaas-fn"
  }
}

resource "kubernetes_namespace" "ns_openfaas" {
  metadata {
    name = "openfaas"
  }
}

resource "kubernetes_secret" "sec_openfaas_creds" {
  metadata {
    name = "basic-auth"
    namespace = "openfaas"
  }
  data = {
    "basic-auth-user: "admin",
    "basic-auth-password": var.openfaas_password
  }
}

resource "helm_release" "rel_openfaas" {
  name = "openfaas"
  namespace = "openfaas"
  chart = "openfaas"
  repository = "https://openfaas.github.io/faas-netes/"

  set {
    name = "functionNamespace"
    value = "openfaas-fn"
  }
  set {
    name = "generateBasicAuth"
    value = "false"
  }
  set {
    name = "basic_auth"
    value = "true"
  }
  set {
    name = "serviceType"
    value = "ClusterIP"
  }
  set {
    name = "ingressOperator.create"
    value = "true"
  }
}
```

The terraform script can be deployed with the following commands:

```bash
terraform init
terraform plan -var openfaas_password='<openfaas_password>' --out out.plan
terraform apply out.plan
```

The terraform script performs the following operations:
1. Creates the `openfaas` namespace
2. Creates the `openfaas-fn` namespace
3. Creates a Kubernetes secret with the `basic-auth` credentials
4. Deploys the OpenFaaS helm template
  - Creates the OpenFaaS stack
  - Disables the generation of a randomized admin password -- instead preferring the `basic-auth` secret we created earlier
  - Deploys the OpenFaaS ingress operator which enables us to ingress our functions using a Custom Resource Definition (CRD)

## Initializing the NextJS function

To create the function which will serve NextJS once we deploy it to OpenFaaS, the Docker template will need to be created.

```bash
faas-cli template store pull dockerfile
faas-cli new <appname> --lang dockerfile
```

The `dockerfile` template is created in a new folder which will be named the same value that was used for `<appname>` in the snippet above.

Next, the NextJS app will be initialized

```bash
npx create-next-app tmp-<appname> --ts # ts is optional. I like Typescript
mv tmp-<appname>/* <appname>/* # Relocate all files into the openfaas function folder
rm -rf tmp-<appname> # temporary folder is no longer needed
```

We have the basis for our NextJS OpenFaas function. The container template files need to be tweaked to work properly.

Update `.dockerignore` to exclude all unnecessary files from the Docker build

```
node_modules
.next
__tests__
coverage
docs
```

Update `Dockerfile` to properly build the NextJS application into an OpenFaaS function

```Dockerfile
# This template was adapted from the original node-express template
# https://github.com/openfaas-incubator/node10-express-template
FROM openfaas/of-watchdog:0.8.2 as watchdog

FROM node:14-alpine as ship

COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
RUN chmod +x /usr/bin/fwatchdog

RUN addgroup -S app && adduser -S -g app app

ENV NPM_CONFIG_LOGLEVEL warn

RUN mkdir -p /home/app

WORKDIR /home/app

RUN yarn

COPY . /home/app/

# Build the server
# remove the dev dependencies
RUN yarn && yarn build \
    && npm prune --production
RUN chown -R app:app /home/app && chmod 777 /tmp

USER app

ENV cgi_headers="true"
ENV fprocess="yarn start"
ENV mode="http"
ENV upstream_url="http://127.0.0.1:3000"

ENV exec_timeout="10s"
ENV write_timeout="15s"
ENV read_timeout="15s"

EXPOSE 8080

HEALTHCHECK --interval=3s CMD [ -e /tmp/.lock ] || exit 1

CMD ["fwatchdog"]

```

With all the configuration completed, you should be able to deploy the function to OpenFaaS

```bash
faas-cli login # Prompt for username and password
faas-cli up -f <appname>.yml # Deploy he function
```

## References

- OpenFaaS Helm Chart - https://github.com/openfaas/faas-netes/tree/master/chart/openfaas
- OpenFaaS Ingress - https://github.com/openfaas/ingress-operator