How Heroku's 12-factor app aligns with modern industry trends
The 12-factor app is a methodology developed by Heroku and was first presented in 2011 by Adam Wiggins. It consists of 12 rules or guidelines that dictate how an app should be built and deployed to improve 2 aspects:
1- App Scalability 2- App Maintainability
Originally, the methodology was created as a must follow for apps to be deployed using Heroku. However, it has been adopted by many other companies either by applying it to their own products or creating new tools that make it easier to apply the methodology on other products. If we watch the current technology trends and commonly used methods in building and deploying apps, we can easily see how they align with those of the 12-factor app. In this article, I will list the 12-factor app rules and how they align with modern trends.
I. Code Base:
One code base tracked in revision control, many deploys.
What this means is that you should not use a mono repo that has multiple apps, but rather have a separate repo for each app. It also dictates that each app can have multiple deployments (ex: staging, production,...) but they must be from the same code base even if they have different commits.
I'm half-hearted about this rule as it has 2 directions:
- Each repo should have only one app.
- Each app should have only one repo even if it has multiple deployments.
As for the first part, I don't think it's strictly better than having a mono repo and currently, many companies have mono repos including Heroku itself according to Heroku's Software Architect Joe Kutner on Episode 409 of the "Software Engineering Radio" Podcast (The episode explains a lot about 12-factor app).
However, I think it's currently a given to apply the 2nd part. It's actually the intuitive action to deploy an app from only one repo even if you have multiple deployments of the app from different commits. Otherwise, there will be a lot of inconsistencies
Explicitly declare and isolate dependencies
An app should never rely on the existence of system-wide tools or packages, but it should be explicitly stating and using it's own dependencies regardless of the packages that exist on the system.
This can be divided into:
- Dependency Declaration
- Dependency Isolation
The first part is about having the app declaring exactly the dependencies and tools that it uses. An example to declaring packages is npm which declares dependencies in
package.json as for declaring tools like curl this is satisfied by using Docker by downloading all the tools you need in the image before deploying.
The second part is about isolating the app dependencies from system-wide packages so the app uses only the its dependencies and doesn't accidentially use a system dependency. Again, Docker satisfies this condition since it separates the app's environment completely from the machine's environment.
Store config in the environment
Config include database URLs, third-party services credentials and per deployment values. These values are mostly different according to each environment
Config should be decoupled from the app itself so changing the database for example would not require changing anything in the code rather it would be a change in the environment itself.
To know if your app satisfies this condition, ask yourself, can you make the app opensource without changing the code?
This is definitely best practice and often github would warn you if you have credentials in your repo. However, it's not uncommon to make the mistake of having config in the app itself but this rule definitely still applies to modern apps for security and scalability reasons.
IV. Backing services:
Treat backing services as attached resources
Backing services are any services the app needs to run normally like databases, caches and messaging services. What this rule states is that these services should be treated as attachables and should be easily changed without changing the code. This is like a followup to the previous rule because having the backing services configs in the environment will satisfy this rule automatically.
V. Build, release, run:
Strictly separate build and run stages
Each deployment should have 3 stages:
- Build: Generate an executable from the code
- Release: The build is combined with the configs
- Run: The app (build + config) is run in execution environment
This rule dictates that the 3 stages should always be separated and should go only in one direction. So changing the code in runtime is not possible and any change has to be done before the build stage.
The code of a release shouldn't be changed either and each release has a unique ID so any change must create a new release with a new ID.
Finally, the run stage deploys the release.
Considering most CI/CD tools, they abide by this rule. When a new commit is merged, the CI/CD tool creates a new build from that commit and gives it an ID. Afterwards, that build can be used to create a new release for the specific deployment environment. For example, you can select to deploy build
v1.0.0 to testing and according to your pipeline, this might create a Docker image that has the build and the config of the testing environment. Later, this build is run on your server through Kubernetes for example.
I would say this is a very important rule specially now that CI & CD are part of most projects and separating the pipeline stages is crucial to be able to easily revert to a specific build/release when needed.
Execute the app as one or more stateless processes
This is a very important rule and in my opinion, this is the foundation of current deployment methodologies.
This rule states that an app should have zero or more processes (instances) and they should be stateless.
Having multiple instances means that the app can handle more requests and scale horizontally instead of vertically which is something that most modern products do. In addition, having the processes stateless, means that they can easily be disposed of and replaced by new instances (as stated in rule IX). A stateless instance doesn't expect any thing to stay in memory or disk but rather uses stateful backing services like databases. Those instances should also never use sticky sessions so a user can have their requests handled by multiple instances and should keep going normally in case an instance is destroyed for any reason.
Obviously, this is how Kubernetes works, Kubernetes allows you to choose the number of instances of the app to run and it manages stopping, restarting and replacing the instances automatically expecting them to be stateless.
VII. Port Binding:
Export services via port binding
This rule states that the webserver should be part of the app.
As a young developer, this rule is very confusing to me and I didn't get it at first. After some research, I learned that previously, the app didn't have to be itself the web server. Rather the app could be deployed somewhere and its web server somewhere else where the web server is responsible for routing to the app. I don't really think this is relevant today since the standard now is implementing the web server into the app itself. However, maybe that was influenced by this rule and that's actually why this technology is no longer used.
Scale out via the process model
Another very important rule relating to the use of microservices.
This rule states that a program should be divided into multiple processes where each process has it's own functionallity. The 12-factor website explicitly states that this is different from in-app concurrency like running multiple threads inside the app. But the concurrency model should follow the "unix process model for running service daemons" where the processes are explicitly created and visible to the developer as processes are first class citizens in the sense that each process should be managed, controlled and configured by the developer (or at least the developer is aware of them) and not internally by the app.
Obviously, this is the fundamental concept of microservices architecture which has not been formally given the name "microservice" until later in 2012.
Maximize robustness with fast startup and graceful shutdown
Another crucial rule that aligns with modern deployment methods.
This rule state that:
- Processes should be disposable. (which we established in rule VI)
- A process should always be ready to gracefully shutdown without breaking the app.
This means that each process should be ready for any unexpected shutdown whether its a SIGTERM or a hardware failure. For example, if a process receives a SIGTERM it should have a mechanism to delegate it's current job to another process like putting it back to the queue so when another process is instantiated to replace it, the job can be finished.
Again, Kubernetes runs expecting processes to do that and this is why processes can be restarted/replaced at any time and why changing the release version of an app is now as easy as running one command (or even use a GUI) to select the release you want and Kubernetes can handle the rest.
X. Dev/Prod Parity:
Keep development, staging, and production as similar as possible
This one is self explainatory. You should always strive to have the development, staging and production environment as similar as possible. This is mainly to make the app ready for CD and to achieve that, you need to minimize 3 gaps between the environments:
- Time Gap: The time between development and deployment to production. This is fixed by minimizing the time between development and deployment.
- Personnel Gap: Development and Deployment are handled by 2 different persons (Developer and DevOPs). This is fixed by involving the developer in the deployment process.
- Tools Gap: Developer might use lightweight tools like (SQLite) while production uses more sophisticated tools like (MySQL). This is fixed by...well, using the same tools for development as in production.
This has become a lot easier now with CD tools that allow developers to handle deployment as soon as their changes are done. Using the same tools in all environments is easier now with packaging systems like homebrew and of course with docker where you can use any service you want without going through setting it up.
Treat logs as event streams
This rule prohibits writing or managing log files, rather it encourages treating logs as events in the sense that they are actually time-ordered events. So the app should just output these events as a stream through stdout and they should be handled outside the app.
Of course this is very relevant today given that processes are disposable and distributed which makes sence to aggreagate logs from different processes in one place.
Services that provide that include Logstash and loggly.
XII. Admin Processes:
Run admin/management tasks as one-off processes
An example to these tasks are DB migrations. The rule states that these tasks must:
- Run in the same release environment as the deployment
- Be part of the app code even if they are a one-time script
This focuses on the consistency of the app where you shouldn't just ssh to the server and run commands there. Rather, you should create scripts that are shipped with the code to make those changes.
If you have ever used an ORM to manage migrations, then you will notice that it follows this rule as the migration scripts are stored in the project and can be run in multiple environments with the same consistency.
The 12-factor App, despite being almost a decade old, is mostly relevant and it has obviously changed the way of implementing and deploying apps and possibly influenced the tools that might be considered as the standard in the current software industry.