DEV Community

Cover image for 12-Factor App For Dummies
Meysam
Meysam

Posted on • Edited on • Originally published at meysam.io

12-Factor App For Dummies

This article was originally published at meysam.io

The 12-factor app is the name the guys at Heroku gave to some set of principles they found helpful while developing "a wide variety of software-as-a-service apps in the wild".

This has to make sense because we know from experience that practice makes perfect. The more you try, the better you become at improving your chance of success and reducing your failure rate. That also leads to a better general feeling which will also make you want to try more, but that's a story for another day and we'll leave it at that.

So, back to what you clicked the link for this article. The 12-factor app, as the name suggests, consists of twelve principles that are gathered and contributed to the community. I appreciate the people who contributed their knowledge to everyone. It'll, generally, improve the principles of software engineering and leads to better apps.

So without any further ado, let's try to introduce these pillars, hopefully with some clear examples to help understand better what they have to say.

The 12-factor app applies to any programming language using any kind of backing system (database, queue, etc.)

1. Codebase

There's one and only one codebase (repository); there can be many deployments based on different configurations (e.g. stage & prod) but it has to be all in one repo. If it's in multiple repositories, it's not an app, it's a distributed system (maybe of several apps).

It translates to being able to run the same app under different configurations. It would only be possible through keeping strict isolation between the app (what you write as code) and the config (what different initialization of env vars would do to that code). We'll talk about configuration in the Config section.

Another point to mention here is that they should not share the same line of code (the DRY principle). If they do need to share some code for some reason, it has to be in a shared library to be imported by different apps. Generally, duplicate codes in different apps is a hard no-no.

2. Dependencies

There should never be an assumption or reliance on the implicit existence of system-wide packages. What it means is that whatever you need in your app, you should explicitly specify it; whether in requirements.txt in Python projects, package.json in NodeJS, etc. you always need to specify what you need down to a specific version of that tool. For example numpy==1.21.3.

Even if you need something external that is not installed with your library, such as nmap, curl or g++ for example, you should specify it somehow somewhere and document the installation of such dependency. For example in a Dockerfile, a bash script, etc.

Declaring dependencies in such an explicit manner makes your app reproducible across different platforms by different users. Otherwise, you'll end up losing your audience even if your app is so cool that'll pop everyone's eyes out.

3. Config

The app and the config must be separated. If there's a specific configuration of how an app would behave in an environment, that config has to be documented somewhere and the operator (DevOps, SRE, etc.) has to be able to initialize it on app startup. Consequently, no hard-coded values should be provided in the code that can otherwise be changed in different deployment environments.

This is easy to manage since various awesome libraries can be used just for such cases. One of my favorite libraries for receiving arbitrary configurations is pydantic (Python). There are also different dotenv libraries for different programming languages.

The way we treat secrets is just the same way. We have different passwords across different deployments of our app (test, dev, stage, prod) and although the security measure is very important, the separation of config & app makes such security practice possible.

A good symptom of knowing if this principle is applied correctly is to see whether or not we can run the app with different configurations without changing a single line of code.

4. Backing Services

Whether or not your database is on localhost, in Cloud, or provided by a third-party partner should make no difference in the way we behave in the app. It should just be a connection string that we receive in an environment variable and use it to connect to any kind of such a database. In short, backing services have to be treated like attached resources.

Running an app alongside an instance of MongoAtlas, or some local Mongo running on Docker or even on bare metal somewhere on the planet should make no difference to the app.

This is the case for many of the apps in today's modern web development because the underlying connectors (e.g. SQLAlchemy) make it so. Nevertheless, it's worth noting that it wasn't the case back in the days (right! like I'm old enough to remember 😁).

5. Build, Release, Run

An app should separate the three stages that are necessary to run the app, regardless of their programming language.

The build is the stage where your code turns into an executable. That could very well be a docker build in the modern world but could also be something where you compile your C++ codes into a CPU-specific executable binary using g++ (do people still write programs in C++? πŸ€”πŸ˜„).

The release is where your executable from the previous step is merged with a config file and ready to be immediately deployed/executed somewhere in a specific environment (e.g. production).

The run stage is where you run the resulting runnable from the previous step which will ultimately result in a process running inside an operating system (hopefully Linux 😜) that can handle/process requests. This is also called the runtime.

In the 12-factor app, there's a clear separation of these stages and with the new modern tools, this is also pretty straightforward. As an example, consider the lifecycle of a Docker image & container; The first stage is to build the image (the build stage). After the build is complete, by providing some set of environmental variables (e.g. .env file) you'll have your release ready to be deployed immediately (release). The final step would be to run the docker image and create a container that is an active entity that processes the requests as programmed.

6. Processes

The running entity from the previous step that handles requests must not be aware of other forks of the same executable and must not share anything. Any data that is needed in-between requests should be stored in a backing service and the processes talking to each other is a clear no-no in the 12-factor app.

It means that different runnable instances of the same app should not be aware of each other and should never talk to each other directly. What this means is that we should only rely on database/caching/etc. for retrieving the data needed to process a request.

This level of isolation is clear and pretty straightforward in our modern web development practices because we no don't keep any information in-between requests and anything that comes to us is treated the same way like it's the first time visiting us. REST is a clear implementation of such good practice and is adopted by many of us already.

7. Port Binding

A 12-factor app is completely self-contained and would listen on a specific port and expose HTTP as a service through that binding. It must not be bound to other external web services such as Nginx, HAProxy, envoy, etc. It should only listen to a specific port for requests and do its magic on its own without reliance on external services.

An example of a non-12-factor app is a PHP app that might run as a module inside HTTPD.

The app would be behind some reverse-proxy/load-balancer in a production environment, but that should not be treated as a dependency inside the app itself.

Obviously, during the recent years and with the rise of docker, this is the de facto of the industry and is easy to maintain and implement.

Having such a principle in the app will make it possible to run the app in a local development machine that will handle requests in the same manner as the production does, even without the need for load balancers or reverse-proxies being present.

8. Concurrency

A 12-factor app is all about processes. Processes are first-class citizens and they are strongly similar to UNIX processes. There can be many processes inside an app such as the web server handling HTTP requests or the background process doing heavy computations, etc., but they should be a process regardless.

This looks like an obvious thing to be doing these days since every application we write is just processes that are spawned and can easily do their job without the inter-process communication thingy.

The sexiest part about this process model (see Process section above), is that it shines when it comes to scalability. We can easily create a new instance of the app in a different physical machine and voila, our app can handle a few tons more requests, doesn't it? (If only the world was only so simple and we didn't have to worry about database consistency πŸ˜‰).

And we all love scalability, don't we? We are thrilled when our app can handle 1 million requests per second. Of course, that's a dream for many of the people in this industry but one can only hope 😁.

9. Disposability

This means that each app should have minimal startup time and can easily shut down gracefully when the SIGTERM signal is received. It increases the development speed and deployment agility. With this capability we are not bound to a state of a single process of an app, but rather every information/context that we need can be accessed via the external backing system (e.g. PostgreSQL, RabbitMQ, etc.).

You can think of it this way: if a Sysadmin terminates the app for whatever reason and turn it back on in half an hour, the app should be able to function as expected from where it left off. Of course in reality this is easier said than done, but we're gonna have to try our best, right?

It can also be a situation where a hardware failure occurs and for such situations, the app should also be able to take care of itself.

10. Dev/Prod Parity

We should have the minimum amount of difference between the development environment and the production. We've all had cases where we develop the app in local machine using SQLite and run the app in production with PostgreSQL, right? Yeah, that's a no-no in the 12-factor app baby! You should use the same technology for both. Do you know why? Because different technologies have different behaviors on applying similar principles. Here's a link to an example.

To be able to program the right behavior in the app in different scenarios, you have to be able to see how different tools and components interact with each other and that is only possible if you put pieces together. Disabling or changing technology in the development environment will narrow down your vision and it might lead to unexpected and unanticipated bugs in production (We don't want a wake-up call at 2 A.M., do we? πŸ€·β€β™‚οΈ).

To make your life easier, here's a list of three usual types of gaps between development and production environment you might see in the wild:

1. Time Gap

Traditional app: The time from a code to be deployed to production is months.
12-factor app: It takes hours or even minutes to see the changes in production.

2. Personnel Gap:

Traditional app: Different teams for development and deployment.
12-factor app: The same guy writes the code as the one who deploys it.

3. Tools Gap:

Traditional app: Badass technologies in prod and lightweight in dev.
12-factor app: The dev/prod parity is as similar as possible.

11. Logs

Don't care where to put logs. Just stream them to the stdout is all that is needed because as far as observability is concerned, the log aggregation technologies take care of how to make a cool view of those logs in a dashboard of your choice. Therefore you should never concern yourself with pitty decisions such as where to store the log file in the filesystem.

The logs are just as the name suggests; a stream of events that are dumped to the screen. The app is not and should not be responsible for beautifying the logs for you. You're the one who should. I'm sorry if that's not what you like to hear but that is the case. The app just writes them and it can move on with his life.

By the way, there are cool tools for log aggregation and dashboard stuff if you're interested. Just look them up, or consider the list below for a reference of your available options.

12. Admin Processes

Running admin/management tasks should run in the same environment as the long-running task that is handling user requests. Remember the times where you needed to add a new superuser to your application using python manage.py createsuperuser? Yeah, that's a management task that should run using the same user, the same environmental variables, the same working directory, and against the same set of tools and technologies.

Don't go yelling at me that this is the case in almost all our developments in the 21st century. I know we newcomers don't remember the days where this wasn't the case. But clearly, they didn't write this principle down just for the sake of writing it. There has been some kind of other bad practice that was around before we came to picture bragging about know everything about everything πŸ˜….

Conclusion

That's it, guys. I hope you find this article useful. I've been planning for so long to complete the reading of the 12-factor principles for a very long time and once I finally dedicated a couple of hours to it, I decided to share what I have gained so that others can enjoy the value.

I know we have all been guilty of violating at least one of the above principles and although we're not living in a perfect world, let's just try our best to adhere to these cool ideas to provide our best service to the world.

Stay tuned for more software engineering articles and as always, keep up being awesome πŸ’ͺ.

Reference

From This Author

Top comments (0)