Using 12-Factor in Your Apps

The 12-Factor App methodology is a list of principles, each explaining the ideal way to handle a subset of your application.

The Twelve-Factor app, written by Heroku co-founder Adam Wiggins, is a methodology for building software-as-a-service apps in modern deployment environments. The principles, written by Heroku engineers, describe the best practices to get a modern web app deployed properly. Here’s everything to know about the 12factor manifesto.

In this post, we will apply theory to practice by using examples from a simple application.

Why is it worth it?

The “Works on My Machine” Certification Program is no longer needed.
This is perfect for a cloud environment because there‘s a clear contract between the app and the other services which surround it. Therefore, if you fulfill your part of the contract, your app will be deployable. Following these principles will make an application not only deployable but also operable. 
The operation will be operable in any of the following situations:

- If you’re in a DevOps atmosphere which is operated by the developer who wrote the code 
- If it’s been operated by your operations team 
- If it’s been operated by a third party company such as Heroku or Netlify.

12factor by example:

1.- Codebase

One codebase tracked in revision control, many deploys
It sounds pretty obvious to software engineers, remember to use Git or any other CVS to track any changes in the code. Again, although this may seem pretty obvious, there should be no question that your infrastructure must be also defined as code. No matter which provisioner or technology you are using, it should also be tracked in code and should also follow the 12factor principles. For the purpose of this article, we’ll be using Ruby and Docker code examples. The whole code can be reviewed in Github.

2.- Dependencies

Explicitly declare and isolate dependencies
In Flywire we mainly use the Ruby language; then to declare and isolate dependencies we use a Gemfile and Gemfile.lock. The idea is to know what is needed by our application to run and which versions provide stability. As you can imagine, this is key when running services in production.

For example, in Gemfile we pin the major version because the new version might not be compatible with our code.

gem ‘puma’, ‘~> 3.10’

In a Dockerfile, we can pin to a specific version of an image to avoid different behaviors. Don’t use the latest version (without specifying a version), or at least in production.

FROM ruby:2.3

3. - Config

Store config in the environment
Your code must be always separated from the configuration. It can then be applied to many different environments with different configurations. In the case of passwords, using this pattern doesn’t mean it’s more secure than storing credentials in a file(that would require a full post to discuss it); but it will definitely be more secure than storing them in the code!

For example, if you do something like this in config/database.rb:

set :database, “postgresql://docker:docker@db/factor”

You must change the code for each different configuration. Instead, use an environment variable which will allow you to easily change the configuration for each deployment.

set :database, ENV[‘DATABASE_URL’]

For Docker I would insert env vars at run time.

docker run -e DATABASE_URL=$DATABASE_URL 12factor-ruby:0.1 or docker run — env-file my_app_keys 12factor-ruby:0.1

Don’t store your credentials inside a Docker image in build time. However, if you do store the following code, do so in a Dockerfile.


ENV DATABASE_URL=$DATABASE_URL.

This will store your credentials in an intermediate layer that can be used by other dockers in the system.

4.- Backing Services

Treat backing services as attached resources

The idea would be to have a code where, you don’t know which backing service you have and if you change it, the code it is not altered. In Ruby you can use tools such as Active Record and configure the database in an environment variable.

def add_country(country_name)
$LOG.info "Creating country #{country_name}"
country = Countries.new
country.name = country_name
country.visits = 0
country.save
$LOG.info "Created country #{country_name}"
end

In config/database.rb :

set :database, ENV['DATABASE_URL']

Like this, the code is completely agnostic to the backend used and if you want to change it to another database system you would simply need to change the DATABASE_URL environment var.

5.- Build, release, run

Strictly separate build and run stages
You must strictly separate the Build (binary), Release (binary and + env config) and Run (exec runtime) stages. As we previously mentioned, our instances must be immutable so that we can’t make changes upstream.

Every change must be a new release with a unique ID.

Let’s do it with Docker (this requires login to a docker registry). You first need to create a build and tag the release.

docker build -t ferrandinand/12factor-ruby:0.1 .

Then you publish the image.


docker push ferrandinand/12factor-ruby:0.1

Finally, use the published image in Running Docker Run, Docker-Compose or in Kubernetes (see the Dev/Prod parity pattern below).

6.- Processes

Execute the app as one or more stateless processes
Twelve-factor processes are stateless and share-nothing. Any data that needs to persist must be stored in a stateful backing service. That’s because resources in a cloud environment are ephemeral and should also be immutable. It, therefore, makes no sense to store files or session data in memory. We have a perfect match with containers because they are designed to run with just one scope, and of course, they are ephemeral.

This would be a bad practice in our container Dockerfile.

VOLUME /logs

And if there’s something similar in your code as well.

log = Logger.new(‘log_file.log’, ‘monthly’)

Remember, if you need to store data, do so with a backing service.

7.- Port binding

Export services via port binding
If we said in the backing service pattern that every service should be accessed via URL, that includes our app. Exporting services via port binding will allow us to become a backing service for another app via url.

So in the Dockerfile we will expose the port 4000 but we will also run Puma server that will be bound to that port

Dockerfile

CMD [“bundle”, “exec”, “puma”, “-p”, “4000”]
EXPOSE 4000

8.- Concurrency

Scale out via the process model
Although it might seem pretty obvious at first remember that if, for any reason, you aren’t able to scale your app horizontally, it won’t be prepared for the cloud. The cloud must be a synonym of automation in order to ensure that we can create replicas of our application on-demand.

9.- Disposability

Maximize robustness with fast startup and graceful shutdown
We must ensure that our applications are able to shutdown cleanly. For instance, we shouldn’t stop an application when it’s writing to a backing service. To do so, our app must be able to capture signals, ensure that we finish calls, and then stop the app.

lib/app_signals.rb

require ‘active_record’
Signal.trap(“TERM”) do
puts “Sending TERM signal to app”
shutdown_app
exit
end
Signal.trap(“INT”) do
puts “Sending TERM signal to app”
shutdown_app
exit
end
def shutdown_app
active_connections = ActiveRecord::Base.respond_to?          (:verify_active_connections!)
puts “Closing connections” if active_connections
puts “No active connections” if !active_connections
ActiveRecord::Base.clear_active_connections! if active_connections
end

To send a signal and test it we can use:

docker kill -s SIGTERM image_id

10.- Dev/Prod parity

Keep development, staging and production as similar as possible. Although it might pretty strange to hear that in Flywire we use a different kind of technology for staging than for production, when it comes to development we sometimes use lightweight software for backing services Nowadays setting up services is very easy thanks to open-source software products such as Vagrant and provisioners such as Chef or Ansible; and even easier with containers. In Flywire, we use Docker-Compose with some of our apps because it allow us to run our applications with all the required services and also allows us to run tests locally on our CI system.

Docker-Compose

version: ‘3’
services:
db:
image: postgres
environment:
POSTGRES_PASSWORD: docker
POSTGRES_USER: docker
POSTGRES_DB: factor
web:
build: .
environment:
DATABASE_URL: postgresql://docker:docker@db/factor
ports:
- “4000:4000”
depends_on:
- db

There’s also a Kubernetes example in the Git repo

11.- Logs

Treat logs as event streams 
Services should never concern themselves with routing or storing logs. Instead, apps should be as agnostic as possible as to not depend on any other system or process. Then our app should just put logs in stdout and if we want to collect and ship them other processes/apps should be in charge of that.

Following this idea would be wrong in our app.

log = Logger.new(‘log_file.log’, ‘monthly’)

The right way would be

log = Logger.new(STDOUT)
log.debug(“Created logger”)

Checking logs with docker is very easy. Just type:

docker log container_name.

It would be pretty similar in the case of Kubernetes. You would just replace it with:

kubectl logs pod_name.

12.- Admin processes

Run admin/management tasks as one-off processes.
Our applications must allow access to run maintenance tasks for the app, run with the code and have the same behavior in all environments.

To create the initial DB structure in:

Docker-Compose

docker exec -it 12factorapp_id bash -c “rake db:migrate”

Kubernetes

kubectl exec -it 12factorapp-655998b74b-wflp8 — bash -c “rake db:migrate”
minikube service factor — url

Heroku

heroku login
heroku run rake db:migrate -a factor12

Post written by Ferran Tomás
Ferran’s been dealing with distributed systems during many years in which he’s pursued the path from private software to open source, bare-metal to virtualization and on-premise to cloud. He’s currently working on containerization … and still enjoying the path.

Like what you read? Give Flywire Engineering a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.