The Twelve Factor App is a methodology for building and delivering software-as-a-service apps. At Ombu Labs, we incorporate the following factors into our software development:
“One codebase tracked in revision control, many deploys”
Your code base should be using a revision control system. We prefer Git and GitHub. We are open to working with other revision control systems.
In terms of environments, we want our clients to have at least four environments:
The staging environment should be as close to production as possible. This gives us the opportunity to run a test script that resembles the behavior in production. Final quality assurance will take place in the staging environment.
On top of that, we recommend that you use a collaboration workflow like Gitflow -- It doesn’t have to be exactly the same, but similar.
Every change to the project should be submitted in the form of a pull request. This gives us the opportunity to have a peer review the changes we submit.
On top of that, a pull request should be as simple as possible and follow the single responsibility principle. Reducing the mass of your pull requests make it easier for us and other developers to review the changes.
“Explicitly declare and isolate dependencies”
Your application should use a package management system. It could be a combination of more than one system (e.g. npm and bundler)
All dependencies must be clearly declared in the
package.json and should be as explicit as possible.
Having a system that clearly declares dependencies allows us to quickly install the necessary libraries. If
bundle install or
npm install are not enough to get up and running, you should have a
./bin/setup script in your application.
./bin/setup should make no assumptions and install all that is needed to run the application in the
development environment. It will create “git ignored” configuration files, set environment variables, and install all required libraries.
A more advanced alternative is to use Docker. Many applications use this tool to get your development environment setup. We love it and encourage you to have an up-to-date Dockerfile in your application. It is even better than
./bin/setup in isolating dependencies and environments.
“Store config in the environment”
There should be a clear separation between configuration and application code. Any value that might vary across environments should be defined as an environment variable.
You could use a
.env file in your application that defines these environment variables. We like to use
dotenv for all of our own applications. This gives us an easy way to alter the environment without touching the application code.
A litmus test for whether an app has all config correctly factored out of the code is whether the codebase could be made open source at any moment, without compromising any credentials.
For Rails applications we usually keep
.env outside of the repository. There should be these corresponding files in the repository:
./bin/setup will usually create these files in your local environment.
“Treat backing services as attached resources”
Every application should have a
Procfile that declares all the services that are necessary for an application. Using
foreman you should be able to quickly start an application using
Alternatively, if your application depends on local and external services, you should distribute your application with a
Dockerfile that spins up all the required services for the application.
This will help us get started as quickly as possible without having to ask your team for permissions to external services.
“Keep development, staging, and production as similar as possible.”
The goal is to keep development, staging, and production as close as possible. There shouldn’t be that many differences between the environments. Any difference should be related to an environment variable (for example: in
development we will use
mt as the SMTP server, in production we will use Sendgrid)
The goal of using small (or even tiny!) pull requests is to reduce the gaps mentioned in 12 factor applications. We focus mostly on these two gaps:
- Time between deployments gap: We want to deploy as many backwards compatible changes as possible. If we can ship a set of changes that doesn’t cause any problems, then it is one less problem we need to worry about in the “version bump pull request”
- Personnel gap: We work in small teams. Every developer will monitor the execution of their own pull requests in staging and production. If something fails, the developer who worked on it will keep track of the issues and submit a patch as soon as possible. The code author will work closely with the code deployer.
“Treat logs as event streams”
Before we start working on a Rails upgrade we will make sure that logging is properly setup in both staging and production.
To make our debugging easier we want to use services that keep track of all exceptions: Airbrake; Sentry; Honeybadger. We want to have access to not only log files, but exception tracking services.
Once we deploy a pull request, we want to closely monitor log files and new exceptions. If anything unexpected happens, we want to get notified about this. It will make our life easier when tracking down new issues.
“Run admin/management tasks as one-off processes”
If any data migration is necessary, we will package it as a rake task under the
data namespace. That way, we will keep database migrations in one place (db/migrate) and data migrations in another place (lib/tasks/data.rake)
Data migration tasks should be idempotent. You should be able to run them as many times as you want and end up with the same result.
We strive to keep data and database migrations separate. Every data migration task should have test coverage to verify that it does what it is supposed to do.
We will keep track of your own admin processes and take that into account when migrating to the new version of Rails. For example: If you stopped relying on full database resets and started relying on
db/structure.sql to get started with the project, we will take that into account.