Self-deployed FaaS with Docker Swarm
Serverless is all the rage right now. Instead of maintaining a server and its infrastructure, you can create self-contained functions that do the job.
This can be a boon for front-end developers. A bespoke front-end client written in React.js or another framework can easily be enhanced with back-end code.
It's now trivial to add a secure integration with a payment provider like Stripe.
You don't have to write, deploy and maintain separate back-end code (e.g., a Node.js and Express.js server).
One of the often-touted advantages of FaaS (Functions as a service) is the price. You only pay for what you're using.
Some providers, like Netlify, even offer a generous free tier.
But what happens when you want to avoid lock-in?
That's where OpenFaaS comes in. OpenFaaS is an open-source framework for functions-as-a-service, using Docker Swarm, Kubernetes or OpenShift.
We can deploy OpenFaaS to a VPS (Virtual Private Server) provider like Upcloud1 or DigitalOcean1. A small instance, which costs around $3 to $5 US dollars, is enough for a self-hosted FaaS setup.
In this post I will show you how to use Docker Swarm to deploy an OpenFaaS cluster. We'll also use Traefik 2 to provision SSL certificates for our functions for free.
Prerequisites
You will need a running cloud server.
I particularly like Hetzner and Upcloud, but you can use what you like.
Upcloud1 has top-notch performance for the 5-dollar-tier and a friendly customer support.
Hetzner has very cheap offerings under USD \$3. Their smallest offering is not very performant, but sufficient for our use case. Keep in mind that Hetnzer's servers are located in Germany and Finland.
For alternatives, check VPSBenchmarks.
Provision an Ubuntu 20.04 server, create and connect an SSH key, and don't forget about basic security.
You'll also want to have a domain. Namecheap is one of the services I use for buying domains, and so far I've been satisfied with them.
You need to find a way to connect your domain to the cloud server you just spun up. How you do it depends on your domain registrar and your VPS provider.
You'll need 3 subdomains for the following services: Traefik dashboard, OpenFaaS dashboard, Prometheus dashboard.
Let's say your domain name is myfaas.xyz
.
Create A records for traefik.myfaas.xyz
, faas.myfaas.xyz
and prometheus.myfaas.xzy
.
(The names for the subdomains are up to you, of course).
Here's a guide that might help you out.
Software Installation
I suggest using a non-root user with sudo access instead of the default root user.
SSH into your cloud server and install the necessary packages:
# Install Docker (https://docs.docker.com/engine/install/ubuntu/)
sudo DEBIAN_FRONTEND=noninteractive apt -y install \
apt-transport-https \
ca-certificates \
curl \
git \
gnupg-agent \
software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io
Give the current user the necessary permissions:
sudo groupadd -f docker
sudo usermod -aG docker "${USER}"
Log out of the session, so that the changes to can take place.
Install OpenFaaS and faas-cli
We'll be using Docker Swarm for deployment.
The default option is Kubernetes because of its richer ecosystem. But right now, I have no clue how Kubernetes works. And this guide shows you a setup for hobby projects, so Kubernetes would be overkill anyway.
I'm familiar with Docker, so Docker Swarm it is.
You can also use faasd, a simpler alternative. faasd uses containerd instead of Docker.
But although Docker uses containerd, they are not the same.
That means that automatic SSL certificate generation via Traefik 2 does not work. Traefik works with a few providers like Docker or Kubernetes, but not with bare-bones containerd.
Edit: What you can do is add Terraform (infrastructure as code) and use it to install Caddy (a server) to provision SSL certificates. Guide here.
Thanks to Alex Ellis, creator of OpenFaaS & faasd, for pointing that out.
We'll now use the official deployment guide for Docker Swarm on the OpenFaaS docs.
1. Install faas-cli
curl -sL https://cli.openfaas.com | sudo sh
2. Docker Swarm Setup
In preparation for the Traefik 2 setup, we'll create the necessary environment variables. The following section is more or less a copy of the guide from dockerswarm.rocks.
docker swarm init
We will use the docker socket proxy by Tecnativa to protect our Docker unix socket.
The socket proxy is a security measure that's encouraged by Traefik.
Get the Swarm node ID of the current node and store it in an environment variable:
export NODE_ID=$(docker info -f '{{.Swarm.NodeID}}')
Now we'll add a tag to the node to ensure that we'll always deploy to the same node. Why?
We need a way to ensure that the SSL certificates are available to the Docker node that runs Traefik (which handles the provision of the certificates).
docker node update --label-add traefik-public.traefik-certificates=true $NODE_ID
Now we need to find a way to create an admin username and password for both the Traefik admin dashboard and the Prometheus monitoring service.
OpenFaaS will automatically provide basic auth for the functions dashboard.
In my example, I will use environment variables. You can also use docker secrets, which is a more secure approach. But it's also trickier to set up and harder to change as Docker secrets are immutable.
export TRAEFIK_ADMINS=admin:$(openssl passwd -apr1)
Here, admin
is your desired username for login. Change it to something else if you like. You will be prompted for a password. The command will hash the password for you.
We'll do the same for the Prometheus dashboard:
export PROMETHEUS_ADMINS=admin:$(openssl passwd -apr1)
We will use Let's Encrypt to generate SSL certificates. Provide an email as unique identifier:
export EMAIL=myemail@email.com
Next up, we'll add the domains for the services. Assuming we want to deploy the Traefik dashboard to traefik.myfaas.xyz
, we create the environment variable like this:
export TRAEFIK_DOMAIN=traefik.myfaas.xyz
Don't forget the Prometheus dashboard and also the domain for the functions:
export PROMETHEUS_DOMAIN=prometheus.myfaas.xyz
export FAAS_DOMAIN=functions.myfaas.xyz
3. Download OpenFaaS
You should be in the home directory of your (non-root) user on your VPS.
Let's use git
to get a copy of OpenFaaS:
git clone https://github.com/openfaas/faas && cd faas
4. Prepare for Traefik and Docker Socket Proxy
The OpenFaaS stack provides you with a docker-compose.yml
. We now need to tweak it for our needs.
This step is quite tricky, because it's easy to get indentation errors when editing yaml files.
First, open the ready-made docker-compose.yml
file and take a look around. You will find several services for the OpenFaaS gateway, Prometheus, etc.
4.1. Adjust Existing Configuration
The default network is functions
. We'll need to add another network for the services that Traefik should make public (the Traefik dashboard, the Prometheus dashboard and the OpenFaaS dashboard).
Find the networks
section at the end of the file and add a new network. Also add a volume forThe SSL certificates:
networks:
+ traefik-public:
+ driver: overlay
+ attachable: true
functions:
driver: overlay
attachable: true
labels:
- "openfaas=true"
+volumes:
+ traefik-public:
Add the traefik-public
network to all service that we can reach via the internet: gateway
, prometheus
, and later traefik
.
Also remove the external facing ports from the services gateway
and prometheus
. Traefik will handle that for us:
gateway:
image: openfaas/gateway:0.18.18
- ports:
- - 8080:8080
networks:
- functions
+ - traefik-public
...
prometheus:
image: prom/prometheus:v2.11.0
...
- ports:
- - 9090:9090
networks:
- functions
+ - traefik-public
Let's now setup Traefik and the Docker socket proxy services:
services:
+ socket-proxy:
+ image: 'tecnativa/docker-socket-proxy:latest'
+ environment:
+ # required permissions
+ NETWORKS: 1
+ SERVICES: 1
+ TASKS: 1
+ POST: 1
+ deploy:
+ restart_policy:
+ condition: on-failure
+ placement:
+ constraints:
+ - node.role == manager
+ networks:
+ - traefik-public
Let's use the security-enhanced socket in the faas-swarm
service:
faas-swarm:
image: 'openfaas/faas-swarm:0.9.0'
networks:
- functions
+ - traefik-public
environment:
read_timeout: 5m5s
write_timeout: 5m5s
DOCKER_API_VERSION: '1.30'
+ DOCKER_HOST: 'tcp://traefik-public:2375'
basic_auth: '${BASIC_AUTH:-false}'
The Traefik setup is significantly more complicated. Explaining all the moving parts is beyond the scope of this post.
I suggest these two posts and the official Traefik documentation to get started.
+ traefik:
+ image: traefik:v2.2
+ command:
+ - --entrypoints.web.address=:80
+ - --entrypoints.websecure.address=:443
+ - --providers.docker=true
+ - --providers.docker.swarmMode=true
+ - --providers.docker.network=faas_traefik-public
+ - --providers.docker.exposedByDefault=false
+ - --providers.docker.endpoint=tcp://socket-proxy:2375
+ - --api.dashboard=true
+ - --log.level=ERROR
+ - --certificatesresolvers.letsencrypt.acme.email=${EMAIL?Variable not defined}
+ - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
+ - --certificatesresolvers.letsencrypt.acme.tlschallenge=true
+ deploy:
+ restart_policy:
+ condition: on-failure
+ placement:
+ constraints:
+ - node.labels.traefik-public.traefik-certificates == true
+ - node.role == manager
+ labels:
+ # Redirect to https
+ - traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)
+ - traefik.http.routers.http-catchall.entrypoints=web
+ - traefik.http.routers.http-catchall.middlewares=redirect-to-https
+ - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
+ - traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true
+
+ # Dashboard
+ - traefik.http.routers.api.entrypoints=websecure
+ - traefik.http.routers.api.rule=(Host(`${TRAEFIK_DOMAIN?Variable not defined}`)
+ - traefik.http.routers.api.middlewares=api-auth
+ - traefik.http.middlewares.api-auth.basicauth.users=${TRAEFIK_ADMINS?Variable not set}
+ - traefik.http.routers.api.service=api@internal
+ - traefik.http.services.gateway.loadbalancer.server.port=8080
+ ports:
+ - 80:80
+ - 443:443
+ networks:
+ - traefik-public
+ volumes:
+ - traefik-certificates:/letsencrypt
+ - /home/$USER/traefik/traefik_conf:/traefik_conf
Finally, we'll need to tell Traefik which containers it should make publicly available. For that feat, we must add labels to the services gateway
and prometheus
:
gateway:
image: openfaas/gateway:0.18.18
...
deploy:
+ labels:
+ - traefik.enable=true
+ - traefik.constraint-label=traefik-public
+ - traefik.http.routers.gateway.entrypoints=websecure
+ - traefik.http.routers.gateway.rule=Host(`${FAAS_DOMAIN?Variable not set`)
+ - traefik.http.routers.gateway.tls.certresolver=letsencrypt
+ - traefik.http.services.gateway.loadbalancer.server.port=8080
The same for the Prometheus dashboard:
prometheus:
...
deploy:
+ labels:
+ - traefik.enable=true
+ - traefik.constraint-label=traefik-public
+ - traefik.http.routers.gateway.entrypoints=websecure
+ - traefik.http.routers.gateway.rule=Host(`${PROMETHEUS_DOMAIN?Variable not set`)
+ - traefik.http.routers.gateway.tls.certresolver=letsencrypt
+ - traefik.http.services.gateway.loadbalancer.server.port=9090
4.2. Final Configuration
This is the final docker-compose.yml
.
I've updated the docker version to 3.8
and ran the file through a yaml prettifier to avoid indentation errors.
version: '3.8'
services:
socket-proxy:
image: 'tecnativa/docker-socket-proxy:latest'
environment:
NETWORKS: 1
SERVICES: 1
TASKS: 1
POST: 1
deploy:
restart_policy:
condition: on-failure
placement:
constraints:
- node.role == manager
volumes:
- '/var/run/docker.sock:/var/run/docker.sock:ro'
networks:
- traefik-public
traefik:
image: 'traefik:v2.2'
restart: unless-stopped
command:
- '--entrypoints.web.address=:80'
- '--entrypoints.websecure.address=:443'
- '--providers.docker=true'
- '--providers.docker.swarmMode=true'
- '--providers.docker.endpoint=tcp://socket-proxy:2375'
- '--providers.docker.network=faas_traefik-public'
- '--providers.docker.exposedByDefault=false'
- '--api.dashboard=true'
- '--log.level=ERROR'
- >-
--certificatesresolvers.letsencrypt.acme.email=${EMAIL?Variable not
defined}
- '--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json'
- '--certificatesresolvers.letsencrypt.acme.tlschallenge=true'
deploy:
restart_policy:
condition: on-failure
placement:
constraints:
- node.labels.traefik-public.traefik-certificates == true
- node.role == manager
labels:
- 'traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)'
- traefik.http.routers.http-catchall.entrypoints=web
- traefik.http.routers.http-catchall.middlewares=redirect-to-https
- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
- >-
traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true
- traefik.http.routers.api.entrypoints=websecure
- >-
traefik.http.routers.api.rule=(Host(`${TRAEFIK_DOMAIN?Variable not
defined}`)
- traefik.http.routers.api.middlewares=api-auth
- >-
traefik.http.middlewares.api-auth.basicauth.users=${TRAEFIK_ADMINS?Variable
not defined}
- traefik.http.routers.api.service=api@internal
- traefik.http.services.gateway.loadbalancer.server.port=8080
ports:
- '80:80'
- '443:443'
networks:
- traefik-public
volumes:
- 'traefik-certificates:/letsencrypt'
- '/home/$USER/traefik/traefik_conf:/traefik_conf'
gateway:
image: 'openfaas/gateway:0.18.18'
networks:
- functions
- traefik-public
environment:
functions_provider_url: 'http://faas-swarm:8080/'
read_timeout: 5m5s
write_timeout: 5m5s
upstream_timeout: 5m
dnsrr: 'true'
faas_nats_address: nats
faas_nats_port: 4222
direct_functions: 'true'
direct_functions_suffix: ''
basic_auth: '${BASIC_AUTH:-false}'
secret_mount_path: /run/secrets/
scale_from_zero: 'true'
max_idle_conns: 1024
max_idle_conns_per_host: 1024
auth_proxy_url: '${AUTH_URL:-}'
auth_proxy_pass_body: 'false'
deploy:
labels:
- traefik.enable=true
- traefik.constraint-label=traefik-public
- traefik.http.routers.gateway.entrypoints=websecure
- >-
traefik.http.routers.gateway.rule=Host(`${FAAS_DOMAIN?Variable not
defined}`)
- traefik.http.routers.gateway.tls.certresolver=letsencrypt
- traefik.http.services.gateway.loadbalancer.server.port=8080
resources:
reservations:
memory: 100M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 20
window: 380s
placement:
constraints:
- node.platform.os == linux
secrets:
- basic-auth-user
- basic-auth-password
basic-auth-plugin:
image: 'openfaas/basic-auth-plugin:0.18.18'
networks:
- functions
environment:
secret_mount_path: /run/secrets/
user_filename: basic-auth-user
pass_filename: basic-auth-password
deploy:
placement:
constraints:
- node.role == manager
- node.platform.os == linux
resources:
reservations:
memory: 50M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 20
window: 380s
secrets:
- basic-auth-user
- basic-auth-password
faas-swarm:
volumes:
- '/var/run/docker.sock:/var/run/docker.sock:ro'
image: 'openfaas/faas-swarm:0.9.0'
networks:
- functions
environment:
read_timeout: 5m5s
write_timeout: 5m5s
DOCKER_API_VERSION: '1.30'
basic_auth: '${BASIC_AUTH:-false}'
secret_mount_path: /run/secrets/
deploy:
placement:
constraints:
- node.role == manager
- node.platform.os == linux
resources:
reservations:
memory: 100M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 20
window: 380s
secrets:
- basic-auth-user
- basic-auth-password
nats:
image: 'nats-streaming:0.17.0'
command: '--store memory --cluster_id faas-cluster'
networks:
- functions
deploy:
resources:
limits:
memory: 125M
reservations:
memory: 50M
placement:
constraints:
- node.platform.os == linux
queue-worker:
image: 'openfaas/queue-worker:0.11.2'
networks:
- functions
environment:
max_inflight: '1'
ack_wait: 5m5s
basic_auth: '${BASIC_AUTH:-false}'
secret_mount_path: /run/secrets/
gateway_invoke: 'true'
faas_gateway_address: gateway
deploy:
resources:
limits:
memory: 50M
reservations:
memory: 20M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 20
window: 380s
placement:
constraints:
- node.platform.os == linux
secrets:
- basic-auth-user
- basic-auth-password
prometheus:
image: 'prom/prometheus:v2.11.0'
environment:
no_proxy: gateway
configs:
- source: prometheus_config
target: /etc/prometheus/prometheus.yml
- source: prometheus_rules
target: /etc/prometheus/alert.rules.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
networks:
- functions
- traefik-public
deploy:
labels:
- traefik.enable=true
- traefik.constraint-label=traefik-public
- traefik.http.routers.gateway.entrypoints=websecure
- >-
traefik.http.routers.gateway.rule=Host(`${PROMETHEUS_DOMAIN?Variable
not defined}`)
- traefik.http.routers.gateway.tls.certresolver=letsencrypt
- traefik.http.services.gateway.loadbalancer.server.port=8080
placement:
constraints:
- node.role == manager
- node.platform.os == linux
resources:
limits:
memory: 500M
reservations:
memory: 200M
alertmanager:
image: 'prom/alertmanager:v0.18.0'
environment:
no_proxy: gateway
command:
- '--config.file=/alertmanager.yml'
- '--storage.path=/alertmanager'
networks:
- functions
deploy:
resources:
limits:
memory: 50M
reservations:
memory: 20M
placement:
constraints:
- node.role == manager
- node.platform.os == linux
configs:
- source: alertmanager_config
target: /alertmanager.yml
secrets:
- basic-auth-password
configs:
prometheus_config:
file: ./prometheus/prometheus.yml
prometheus_rules:
file: ./prometheus/alert.rules.yml
alertmanager_config:
file: ./prometheus/alertmanager.yml
networks:
traefik-public:
driver: overlay
attachable: true
functions:
driver: overlay
attachable: true
volumes:
traefik-public: null
secrets:
basic-auth-user:
external: true
basic-auth-password:
external: true
Deploy Functions
You can deploy new functions via the dashboard. It's available under the url of the FAAS_DOMAIN
environment variable.
OpenFaaS automatically created an admin user with password. You should have seen the credentials in your terminal. If don't have have them, check the troubleshooting guide for forgetting the gateway password.
You can also use the faas-cli
command-line utility. It offers pre-made templates and convenience wrappers for pushing to a container registry.
More info on the official website.
Final Thoughts
Docker Swarm, Traefik 2 and OpenFaaS play very well together. OpenFaaS offers you an agnostic framework for serverless functions without vendor lock-in. The faas-cli
and the dashboard are very user-friendly.
Traefik 2 shows all its power as a load-balancer and edge router with automatic provisioning of SSL certificates via Let's Encrypt. It plays well with the OpenFaaS architecture.
You should give it a try.
Further Reading
- OpenFaaS: Deployment guide for Docker Swarm
- How To Install and Secure OpenFaaS Using Docker Swarm on Ubuntu 16.04
- Traefik Proxy with HTTPS
- How to install Traefik 2.x on a Docker Swarm
- How to expose Traefik 2.x dashboard securely on Docker Swarm
-
affiliate links - you'll get free credit and I also get some - thank you! ↩
Top comments (0)