Factorish and The Twelve-Fakter App
January 6, 2015
Unless you’ve been living under a rock (in which case I envy you) you’ve heard a fair bit about The Twelve-Factor App. A wonderful stateless application that is completely disposable and can run anywhere from your own physical servers to Deis, Cloud Foundry or Heroku.
Chances are you’re stuck writing and running an application that is decidely not 12Factor, nor will it ever be. In a perfect world you’d scrap it and rewrite it as a dozen microservices that are loosely coupled but run and work indepently of eachother. The reality however is you could never get the okay to do that.
Fortunately with the rise of Docker and its ecosystem it has become easier to not only write 12Factor apps, but also to fake it by producing a Docker container that acts like a 12Factor app, but contains something that is decidedly not. I call this the 12Fakter app.
I’ve been playing with this concept for a while, but over Christmas I spent a bunch of time trying to figure out the best ways to fake out the 12 Factors and feel that I’ve come up with something that works pretty well and in the process created a Vagrant based development sandbox called Factorish which I used to create 12fakter-wordpress and elk_confd.
Fakter I. Codebase
One codebase tracked in revision control, many deploys
The goal here is to have both your app and deployment tooling in the same codebase which is stored in source control. This means adding a Dockerfile
, and Vagrantfile
and other pieces of tooling into your codebase. If however you have a monolithic codebase that contains more than just your app you can create a seperate codebase ( use git! ) containing this tooling and have that tooling collect the application from its existing codebase.
You should be able to achieve this by either merging Factorish into your existing git repo, or fork it and use the Dockerfile
in it to pull the actual application code in as part of the build process.
Fakter II. Dependencies
Explicitly declare and isolate dependencies
This is a really easy win with Docker, The very nature of Docker both Explicitly declares your dependencies in the form of the Dockerfile
and Isolates them in the form of the built Docker image.
Declaration
/app/example/Dockerfile
FROM python:2
# Base deps layer
RUN \
apt-get update && apt-get install -yq \
make \
ca-certificates \
net-tools \
sudo \
wget \
vim \
strace \
lsof \
netcat \
lsb-release \
locales \
socat \
supervisor \
--no-install-recommends && \
locale-gen en_US.UTF-8
# etcdctl and confd layer
RUN \
curl -sSL -o /usr/local/bin/etcdctl https://s3-us-west-2.amazonaws.com/opdemand/etcdctl-v0.4.6 \
&& chmod +x /usr/local/bin/etcdctl \
&& curl -sSL -o /usr/local/bin/confd https://github.com/kelseyhightower/confd/releases/download/v0.7.1/confd-0.7.1-linux-amd64 \
&& chmod +x /usr/local/bin/confd
ADD . /app
WORKDIR /app
# app layer
RUN \
useradd -d /app -c 'application' -s '/bin/false' app && \
chmod +x /app/bin/* && \
pip install -r /app/example/requirements.txt
# Define default command.
CMD ["/app/bin/boot"]
# Expose ports.
EXPOSE 8080
You might notice I have sets of commands joined together with &&
in my Dockerfile
, I do this to control the docker layers more to try and end up with fewer more meaningful layers.
Isolation
$ docker build -t factorish/example example
Sending build context to Docker daemon 20.99 kB
Sending build context to Docker daemon
Step 0 : FROM python:2
---> 96e13ecb4dba
...
...
Step 8 : EXPOSE 8080
---> Running in 8dc9a04eaf78
---> 374cb835239c
Removing intermediate container 8dc9a04eaf78
Successfully built 374cb835239c
Fakter III. Configuration
Store config in the environment
Another easy win with Docker. You can pass in environment variables in the Dockerfile
as we as when running the docker container using the -e
option like this:
$ docker run -d -e TEXT=bacon factorish/example
However chances are your app reads from a config file rather than environment variables. There are [at least] two fairly simple ways to achieve this.
sed inline replacement
use a startup script to edit your config file and replace values in it with the values of the environment variables using sed
before runnin your app:
/app/bin/boot
#!/bin/bash
sed -i "s/xxxTEXTxxx/${TEXT}" /app/example/example.conf
python /app/example/app.py
confd templating
confd is a tool written specifically for templating config files from data sources such as environment variables. This is a much better option as it also opens up the ability to use service discovery tooling like etcd (also supported in Factorish) rather than environment variables.
/app/conf.d/example.conf.toml
[template]
src = "example.conf"
dest = "/app/example/example.conf"
keys = ["/services/example"]
/app/templates/example.conf
[example]
text: {{ getv "/services/example/text" }}
The {{ }}
syntax above is the golang/confd macros used to perform tasks like fetching variables from etcd or environment.
/app/bin/boot
#!/bin/bash
confd -onetime
python /app/example/app.py
Fakter IV. Backing Services
Treat backing services as attached resources
Anything that is needed to store persistent data should be treated as an external dependency to your application. As far as your app is concerned there should be no difference between a local MySQL server or Amazon’s RDS.
This is easier for some backing services than others. For example if your app requires a MySQL database its relatively straight forward. Whereas a local filesystem for storing images is harder, but can be solved:
- Docker: volume mounts, data containers
- Remote Storage: netapp, nfs, fuse-s3fs
- Clustered FS: drdb, gluster
- Ghetto: rsync + concerned
The docker volume mounts actually work really well in a vagrant based development environment because you can pass your code all the way into the container from your workstation, however there are definitely some security considerations to think about if you want to do volume mounts in production.
Example
A fictional PHP based blog about bacon requires a database and a filestore:
/app/templates/config.php
define('DB_NAME', '{{ getv "/db/name" }}');
define('DB_USER', '{{ getv "/db/user" }}');
define('DB_PASSWORD', '{{ getv "/db/pass" }}');
define('DB_HOST', '{{ getv "/db/host" }}');
Docker Run command
$ docker run -d -e DB_NAME=bacon -e DB_USER=bacon \
-e DB_PASSWORD=bacon $DB_HOST=my.database.com \
-v /mnt/nfs/bacon:/app/bacon factorish/bacon-blog
confd will use the environment variables passed in via the docker run
command to fill out the variables called in the {{ }}
macros. Note that confd transforms the environment variables so that the environment variable DB_USER
will be read by {{ getv "/db/user" }}
. This is done to normalize the macro across the various data source options.
Fakter V. Build, Release, Run
Strictly separate build and run stages
Build
Converts a code repo into an executable bundle. Sound familiar? Yup, we’ve already solved this with our Dockerfile
.
Release
Takes the build and combines it with the current configuration. In a purely docker based system this can be split between the Build (versioning and defaults) and Run (current config) stages. However systems like Heroku and Deis have a seperate step for this which they handle internally.
Run
Runs the application by launching a set of the app’s processes against a selected release. In a docker based system this is simply the $ docker run
command which can be called via a deploy script, or a init script (systemd/runit) or a scheduler like fleet or mesos.
Fakter VI. Processes
Execute the app as one or more stateless processes
Your application inside the docker container should behave like a standard linux process running in the foreground and be stateless and share-nothing. Being inside a docker container means that this is hidden and therefore we can fairly easily fake this but you do need to think about process management and logging which are discussed later and is further explored here.
Fakter VII. Port binding
Export services via port binding
Your application should appear to be completely self contained and not require runtime injection of a webserver. Thankfully this is pretty easy to fake in a docker container as any extra processes are isolated in the container and effectively invisible to the outside.
It is still preferable to use a native language based web library such as jetty (java) or flask (python) but for languages like PHP using apache or nginx is ok.
Docker itself takes care of the port binding by use of the -p
option on the command line. It’s useful to register the port and host IP to somewhere ( etcd ) to allow for loadbalancers and other services to easily locate your application.
Fakter VIII. Concurrency
Scale out via the process model
We should be able to scale up or down simply by creating or destroying docker containers containing the application. Any upstream load balancers as an external dependency would need to be notified of the container starting ( usually a fairly easy API call) and stopping. But these are external dependencies and should be solved outside of your application itself.
Inside the container your application should not daemonize or write pid files (if unavoidable, not too difficult to script around) and use tooling like upstart
or supervisord
if there is more than one process that needs to be run.
Fakter IX. Disposability
Maximize robustness with fast startup and graceful shutdown
Docker helps a lot with this. We want to ensure that we’re optimized for fast yet reliable startup as well as graceful shutdown. Your app should be able to be shut down gracefully when docker kill
is called and just as importantly there should be minimal if any external effect if the application crashes or stops ungracefully.
The container itself should kill itself if the app inside it stops working right. If your app is running behind a supervisor this can be a achieved with a really lightweight healthcheck script like this.
/app/bin/healhthcheck
#!/bin/bash
while [[ ! -z $(netstat -lnt | awk "\$6 == \"LISTEN\" && \$4 ~ \".$PORT\" && \$1 ~ \"tcp.?\"") ]] ; do
[[ -n $ETCD_HOST ]] && etcdctl set /service/web/hosts/$HOST $PORT --ttl 10 >/dev/null
sleep 5
done
kill `cat /var/run/supervisord.pid`
You’ll note that I’m also publishing host and port values to etcd if $ETCD_HOST
is set. This can then be used to notify loadbalancers and the like when services start or stop.
Fakter X. Dev/prod parity
Keep development, staging, and production as similar as possible
By following the previous fackters we’ve done most of the work to make this possible. We use Vagrant in development to deploy your app (and any backing services) using the appropriate provisioning methodology ( the same ones we’d use for production).
By wrapping the application in a docker container it is portable across just about any system that is capable of running docker.
By provisioning with the same tooling to both dev and prod (and any other envs), any deployment of development (should happen frequently) is also a test of most of the tooling used to deploy to production.
Fakter XI. Logs
Treat logs as event streams
Your application ( even inside the container ) should always log to stdout. By writing to stdout of your process we can utilize the docker logging subsystem which when combined with tooling like logspout makes it very easy to push all logs to a central system.
If your app has to write to a logfile you should be able to configure that log file to be /dev/stdout
which should cause it to write to stdout of the process. If your app only writes to syslog then configure it to write to a remote syslog. Basically do whatever you can to ensure you don’t log to the local filesystem.
Example
This example shows running Supervisord
as your primary process in the docker container and nginx
writing logs to stdout which in turn are written to the containers stdout
. A more thorough writeup on using supervisor inside docker containers can be found here:
/etc/supervisor/conf.d/nginx
[supervisord]
logfile=/dev/null
pidfile=/var/run/supervisord.pid
nodaemon=true
[program:nginx]
command=/usr/sbin/nginx
redirect_stderr=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
auto_start=true
autorestart=true
user=root
/etc/nginx/sites-enabled/app
worker_processes 1;
daemon off;
error_log /dev/stdout;
http {
access_log /dev/stdout;
server {
listen *:8080;
root /app/bacon-blog;
index index.php;
}
}
For a more detailed post on using logspout to produce consumable logs check out @behemphi’s blog post - Docker Logs – Aggregating with Ease
Fakter XII. Admin processes
Run admin/management tasks as one-off processes
This one is pretty easy. Tasks such as database migrates should be run in one off throw-away containers.
$ docker run -t -e DB_SERVER=user@pass:db.server.com myapp:1.3.2 rake db:migrate
Conclusion
Most of the fakters above are relatively straight forward to utilize and can be built upon slowly, no need to perfect things before working on them. They can also be utilized with any existing provisioning / config management tooling that you already have.
If you’re already using chef for deploying your application you can use the docker cookbook to start running docker containers instead and write out confd templates rather than the final config file which confd will then use to do the final configuration of your app from the environment variables you pass through to the docker_run
resource in the cookbook.
Making your application act like a 12Factor app may not be enough to run it on a purely hosted PAAS like Heroku, but chances are you’ll be able to run it on a Docker based PAAS like Deis. You can go full stack with Mesos or CoreOS+Fleet+ETCD or you can stick to Ubuntu servers running docker.
The flexibility that the 12fakter application gives you means that you can move to a more modern infrastructure at your own pace when it makes sense without having to abandon or completely rewrite your existing applications.
Please check out Factorish and some of the example 12fakter apps like 12fakter-wordpress and elk_confd. to see how easy it can be to start making your applications act like 12Factor apps.