Developing a NextJS app on OpenFaaS

Prerequisites

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

Personally, I prefer to leverage VS Code 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.

{
"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"
}

Creating the OpenFaaS Deployment

The first step in deploying an application to OpenFaas is to deploy the OpenFaaS platform to Kubernetes. I use Helm and Terraform to create the OpenFaaS deployment.
OpenFaaS provides a helm chart

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:

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.

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

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

# 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

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

References

27