Learn how to configure your OpenFaaS functions for different environments
Overview
In this article I’ll explain the differences between confidential and non-confidential configuration options in OpenFaaS. You’ll learn when and how to use both to provide separate configuration for staging and production.
What kinds of things do we need to configure?
Functions are stateless and may make use of external services to store state or synchronise messages:
- Databases - to update or store state
- Queues - for message passing between other parts of your system
- Storage such as S3-compatible endpoints
Functions may also have configuration describing how they should operate such as:
- Log verbosity - do we want more or less detail in staging vs production?
- Timeouts - should we have a lax timeout for local development, but be strict in production?
- Authentication mode - could authentication get in the way of fast, local iteration?
- Faking it - should we be able to fake dependencies on other systems like Stripe or a third-party API?
A third type of configuration may be data stored in a database or a separate system. That’s out of scope for this article, and I’ll focus just on what you can input via the OpenFaaS API.
For the first set of use-cases, we’re primarily talking about confidential data. Things that we would not want in the hands of a hostile actor.
- A password for a Mongo database
- A secret API key for connecting to AWS
For the second set of use-cases, it’s less important if this data were to leak or be compromised:
- Is debugging verbose or quiet?
- What is the URL for an internal REST API?
- What timeout is valid for this function?
There may still be an argument for making some of these things confidential, and there’s no hard and fast rule.
I’m just going to make you aware of the difference, and then show you how to set up either for different environments.
The use-case
Let’s imagine that we have created some marketing automation that collects emails in return for a coupon code for an eBook. During local testing, I can install Postgresql using arkade install postgresql
- it’ll be completely free for me to run this on my laptop. For production I will use DigitalOcean’s managed Postgresql service with backups enabled and highly-availability - it’ll cost me several hundred dollars per month.
The database will have:
- Database name
- Endpoint - DNS endpoint and TCP port
- Whether SSL is enabled
- Username and password
The Staging configuration for our database and marketing function uses a disposable, local database.
So how do we configure this using the standard OpenFaaS CLI and its stack.yml file?
Confidential configuration
In OpenFaaS confidential configuration is defined through a secret. The value of a secret cannot be retrieved by the OpenFaaS API, and when using Kubernetes, its backing store (etcd) can encrypt the data.
Secrets are attached to containers via files under /var/openfaas/secrets/NAME
.
It’s debatable whether the URL for your database is confidential. For argument’s sake, let’s say that we only want to protect the password.
faas-cli secret create \
marketing-db-password \
--from-file marketing-db-password.txt \
--trim
You must first create a secret, then you can attach it to your function via its YAML file:
list:
lang: python
handler: ./list
image: ghcr.io/example/marketing-list:0.2.0
secrets:
- marketing-db-password
Then we can consume the string from our code by reading the file:
read_secret(key):
with open('/var/openfaas/secrets/{}'.format(key), 'r') as f:
return f.read().strip()
return value
How do we have separate values for production and staging?
We can either set different OpenFaaS gateway URLS and text files for the source password:
faas-cli secret create \
marketing-db-password \
--gateway https://staging.example.com \
--from-file marketing-db-password-stag.txt \
--trim
faas-cli secret create \
marketing-db-password \
--gateway https://prod.example.com \
--from-file marketing-db-password-prod.txt \
--trim
With this approach, we only need one stack.yml
file with the secret listed, and change the gateway field.
Or we use different namespaces within the same cluster and OpenFaaS URL:
faas-cli secret create \
marketing-db-password \
--gateway https://prod.example.com \
--namespace staging \
--from-file marketing-db-password-stag.txt \
--trim
faas-cli secret create \
marketing-db-password \
--gateway https://prod.example.com \
--namespace prod \
--from-file marketing-db-password-prod.txt \
--trim
With this approach, we can still use one stack.yml
file, but we need to change the namespace field using a flag.
Non-confidential configuration
For non-confidential configuration, we can use the environment
section of the stack.yml file:
list:
lang: python
handler: ./list
image: ghcr.io/example/marketing-list:0.2.0
environment:
DB_HOST: postgresql.svc.local:5432
But how can we make DB_HOST
take two different values? One for my local Postgresql installed with arkade, and one for the DigitalOcean version?
From speaking to users, I learned of many workarounds for this, but we do have a way to support this in the project already.
The Production configuration for our database and marketing function uses an expensive, highly available, managed database.
First, environment
can be changed to environment_file
to source the data from an external file:
db_envs.yaml
environment:
DB_HOST: postgresql.svc.local:5432
list:
lang: python
handler: ./list
image: ghcr.io/example/marketing-list:0.2.0
environment_file:
- db_envs.yaml
This makes the actual value external to the file, but we still need two copies.
That’s where a very simple but powerful feature comes in. Environment variable substitution.
db_envs_stag.yaml
environment:
DB_HOST: postgresql.svc.local:5432
DB_MODE: ""
DB_NAME: postgresql
db_envs_prod.yaml
environment:
DB_HOST: todo-pg11-do-user-2197152-0.b.db.ondigitalocean.com:25060
DB_MODE: sslmode=require
DB_NAME: defaultdb
Then notice how the filename for environment_file
changes:
list:
lang: python
handler: ./list
image: ghcr.io/example/marketing-list:0.2.0
environment_file:
- db_envs_${OF_ENV:-stag}.yaml
The text db_envs_${OF_ENV:-stag}.yaml
will evaluate to stag
by default, unless the environment variable OF_ENV
is set.
# Deploy to staging using the default
faas-cli deploy
# Deploy to staging using an override
OF_ENV=stag faas-cli deploy
# Deploy to production
OF_ENV=prod faas-cli deploy
To add more environments, simply add more named files.
Alternative ways to deploy functions
We’ve now explored how to define confidential and non-confidential configuration for functions for multiple environments using faas-cli
.
Some other ways you can deploy:
OpenFaaS also has a REST API which is documented in Serverless For Everyone Else along with examples on usage.
Wrapping up
I have a few questions for you:
- How does your team configure your functions for different environments?
- What CI/CD tools do you use to deploy your code?
- Have you implemented any additional tooling or optimizations? What would make life easier for you?
You may also like my tutorial on building and deploying multi-arch functions via GitHub Actions: Build at the Edge with OpenFaaS and GitHub Actions
If you have questions, comments or suggestions, please reach out via Twitter @alexellisuk