Environment Variable Templates
12Factor Config for any Docker Container
Inspiration
I’ve started to deploy CoreOs clair to analyze the security of my docker containers. This service is distributed as a Docker Container, which will allow me to easily deploy it into my stack, but it doesn’t support my platform’s configuration model. Clair stores its config in YAML files and (at the time of this writing) doesn’t support loading its config from the environment. I’m going to show a way to make dockerized services like this behave well with Twelve-Factor Section III (storing application configuration in Environment Variables.)
I’ve found a few examples using these techniques to inject values from environment variables into templatized config files:
They all follow the same model: rewrite application configuration before starting the service. Docker makes this easy by letting us wrap third-party containers with our own config templates and custom entrypoints.
Environment Variable Templating
Here’s a sample clair config file:
1 2 3 4 | clair: database: options: source: postgresql://user@password:host:port/dbname |
I’ll describe a few ways to populate that static config from your runtime environment variables:
For the Unix Neckbeards: sed (stream editor)
Sed is pre-installed on absolutely everything, including Alpine (as part of busybox). It’s been used for this very purpose for decades. It’s great at replacing strings in files, which covers most of your templating tasks. Use it when you can’t be bothered to install anything better. Here’s a template where we delimit environment variables with percent signs:
1 2 3 4 | clair: database: options: source: "%%%CLAIR_DATABASE%%%" |
You could use sed
to replace that string with the value of an environment
variable matching $CLAIR_DATABASE
in multiple ways. To replace a single value
in a file (repeat as necessary):
1 | sed -i "s/%%%CLAIR_DATABASE%%%/${CLAIR_DATABASE}/g" config.template.yaml |
Or to replace all strings matching environment variable keys:
1 2 3 4 5 6 7 8 9 | template = "config.template.yaml" # Read env as key/val variables while IFS='=' read -r key val; do # Replace %%%key%%% with val in template if grep -q "%%%${key}%%%" $template; then sed -i "s#%%%${key}%%%#${val}#g" $template fi done < <(env) |
You can fake bash-style variable substitution using sed. This would replace
${ENV_VAR}
:
1 | sed -i 's/^\${'"$key"'}/'"$val"'/g' config.template.yaml |
Don’t forget to clean up any template placeholders that weren’t populated from the environment:
1 2 | sed -i 's/%%%[^%]*%%%##g' config.template.yaml sed -i 's/^\${\([^}]*\)}//g' config.template.yaml |
For the Hipster Hackers: envsubst
I’ve been using *nix for 20 years and have never heard of envsubst
until I
started writing this blog post. It’s provided by gettext, which isn’t in Alpine
by default, but only adds around 17MB to install. It evaluates and replaces
strings that look like bash variables ($VAR
and ${VAR}
) from the
environment. Use envsubst
when you want basic bash-style variable expansion:
1 2 3 4 5 6 | (~)$ echo $OMG HAX (~)$ echo '$OMG' $OMG (~)$ echo 'OMG: ${OMG}' | envsubst OMG: HAX |
With envsubst
your config.template.yaml
can look a lot more like you’d
expect:
1 2 3 4 | clair: database: options: source: postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}/$DB_NAME |
When invoked with a properly set up environment:
1 2 3 4 5 6 7 8 9 10 | (~)$ cat config.template.yaml clair: database: options: source: postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}/$DB_NAME (~)$ envsubst < config.template.yaml | tee config.yaml clair: database: options: source: postgresql://postgres:p4ssw0rd@pgdb01/clair |
But what if your environment isn’t properly set up? What about those nice
bashisms like default values when variables are missing (e.g.
${VAR:-default}
)?
For the Lulz: bash eval cat
Here’s where I think things start getting fun (thanks,
plockc!) I can’t really recommend this
approach, I mean it screams of security issues, but it’s certainly the most
extensible. This requires proper bash (not ash), which adds about 18mb to
alpine:3.4
, but is included pretty much everywhere else:
1 2 3 4 | eval "cat <<EOF $(<config.template.yaml) EOF " | tee config.yaml 2> /dev/null |
Here’s our new template:
1 2 3 4 5 | # Template generated: $(date) clair: database: options: source: ${DB_SCHEME:-postgresql}://${DB_USER}:${DB_PASS}@${DB_HOST}/$DB_NAME |
This allows you to get really creative, not just using environment variables, but all forms of shell expansion including default values and command substitution:
1 2 3 4 5 | # Template generated: Thu Aug 18 02:48:23 UTC 2016 clair: database: options: source: postgresql://postgres:p4ssw0rd@pgdb01/clair |
For Everyone Else: dockerize
I wrote this whole post to show off some nifty sed
magic. Along the way I
found envsubst
and the bash eval cat
method described above. That doesn’t
mean any of those are the right tools for the job. That would be
dockerize. It’s a static binary that
gives you Golang
text/template for 7.6MB. Our
example template rewritten for dockerize:
1 2 3 4 | clair: database: options: source: {{ default .Env.DB_SCHEME "postgresql" }}://{{ .Env.DB_USER }}:{{ .Env.DB_PASS }}@{{ .Env.DB_HOST }}/{{ .Env.DB_NAME }} |
Running dockerize:
1 2 3 4 5 | (~)$ ./dockerize -template config.template.yaml:config.yaml cat config.yaml clair: database: options: source: postgresql://postgres:p4ssw0rd@pgdb01/clair |
Docker Wrappers
Now that you’ve decided on a template format and tool you need to create your
docker wrapper. The clair
Dockerfile
ends with a simple ENTRYPOINT ["clair"]
statement. We’ll evaluate our
template then invoke that command ourselves with a custom entrypoint.sh
:
1 2 3 4 5 6 7 8 9 | #!/bin/bash eval "cat <<EOF $(</config/config.template.yaml) EOF " | tee /config/config.yaml 2> /dev/null exec "$@" |
Add it all together with a custom Dockerfile:
1 2 3 4 5 6 | FROM quay.io/coreos/clair:v1.2.2 ADD entrypoint.sh / ADD config.template.yaml /config/ ENTRYPOINT ["/entrypoint.sh", "clair"] |
But really, I’m not even kidding, use dockerize:
1 2 3 4 5 6 7 | FROM quay.io/coreos/clair:v1.2.2 RUN curl -sL https://github.com/jwilder/dockerize/releases/download/v0.2.0/dockerize-linux-amd64-v0.2.0.tar.gz \ | tar zxf - -C /bin/ ADD config.template.yaml /config/ ENTRYPOINT ["dockerize", "-template", "/config/config.template.yaml:/config/config.yaml", "clair"] |