The times when we built a separate ecosystem for each application are (thank goodness) over. Hardly anyone nowadays sets up a server from scratch for a web application and installs a whole battery of dependencies by hand. Instead, we increasingly rely on platforms and “platform as a service” providers.
However, for our applications to be able to use their full potential, there are some prerequisites we need to create if we want to achieve our goal: integrating our software into its environment as “quietly” as possible.
The so-called “Twelve-Factor App” principles are a summary of principles that help us with this: Principles that have proven themselves and ensure that we as developers can concentrate on developing.
The History of the Twelve-Factor App
The principles of a twelve-factor app originate at Heroku, one of the first platform-as-a-service providers. They were first presented in 2011 by Adam Wiggins, one of Heroku’s founders and former CTO.
Although they initially seemed (and were) very much geared towards deploying applications as smoothly as possible on Heroku as a platform, they are general enough to also serve as advice for deployment to other providers. Choose the right Web Application Development Dubai by Shaz Tech.
We can even go one step further: Many of the ideas and principles can also be transferred and used for applications that are not deployed by platform-as-a-service providers but on “classic” systems, even in such areas of application to make the development and deployment process easier and more convenient.
So let’s take a closer look at the twelve principles.
1. Code Base
A code base in exactly one repository under version control always serves as the basis for a twelve-factor app. New application versions are built (ideally with each commit) from this one code base, which is then deployed on the target system.
One code base is a source for different target systems, such as the test and production systems. The local execution of the application on a developer’s computer is “only” another target system that is “served” from the code base.
Each deployment, i.e. each running application version, can be assigned to a specific point in time (a commit) within the version history. Anomalies or defects in the production environment can therefore be immediately transferred to a version, making it much easier to find the corresponding functionality in debugging.
Almost no modern application can do without external dependencies these days.
Managing these dependencies is an art in itself. In the past, libraries, frameworks or other resources were often obtained and controlled manually, but now almost all development platforms have their own dependency management applications.
Whether Maven for Java, RubyGems for Ruby, Hex for Elixir or various other package managers – they all have the same goal: to declare dependencies. The package manager makes the actual resolution of these dependencies, i.e. obtaining the necessary resources. The developer can focus on defining what to use rather than worrying about how the dependency will be included and where it can be.
Just like the actual sources, the declaration of the dependencies is part of the code base, which not only shows the status of your source files at any point in time but also exactly which version of which dependencies were used when creating an application.
Declaring dependencies explicitly also means that there can’t be any further or additional undeclared dependencies. All assumptions about the environment in which the application later runs must also be expressed declaratively and understood by the target system. This is the only way to ensure a reproducible system and application structure.
In section 1, we defined that an application is built from exactly one code base but can be deployed in multiple target environments. However, certain parts of the application should still behave differently depending on the target environment in which they are executed.
A simple manifestation of such a different configuration per environment is the database used. Finally, another database is to be used on the test system than on the production system.
The exact configuration of the database (and every other configuration element) in a twelve-factor app does not take place within the application itself but is always passed to the application from the outside.
The application, built from a defined status of the code base, is always identical for the different target environments. For example, a Java application will use the same JAR archives for the test and production environments. The specific configuration items for these same environments are defined in the background for the application and can therefore be read by the application itself as environment variables. The values read from the environment variables can then be used for any actions within the application.
4. Backing Services
Most applications require external services to provide their full functionality. The best-known example of this is certainly a database. Applications use a database as a separate service to persist their data. We also speak of so-called backing services. A backing service is an additional service that is not directly part of an application but is expected.
A twelve-factor app expects such backing services to be provided by the runtime environment and configured via configuration entries in the environment variables (see section 3 ). The application does not take care of the provision and the life cycle of backing services but “only” use the services made available by the environment.
A typical example of an environment variable provided by the runtime environment is the connection URL of a database via the environment variable “DATABASE_URL” (e.g. “postgres://user:[email protected]/database”).
Other backing services provided via the runtime environment are queues and topics, log aggregators, and seemingly banal things like a file system. The runtime environment may take over an automatic backup or automatic enlargement or reduction of storage media but requires the application to only write data to certain directories defined by the configuration.
Again, the goal is for the application to focus on its core functionalities to delegate things like managing databases or the file system to the runtime environment.
5. Build, Release, Run
A twelve-factor app clearly distinguishes the application’s different states, the so-called “run stages”.
During the Build stage, all necessary artefacts of the application are built and packaged. For example, compiled languages create the actual binary in this phase, and web applications (whether compiled or interpreted) can create assets and/or run further build pipelines here. External dependencies are also resolved in the build phase using package managers and similar tools (see Section 2 ).
We refer to the final packaging of all generated artefacts and the configuration elements for a specific environment as a release. A release always has a unique identifier and contains the executable applications and a set of configuration entries.
Therefore, a change in the actual application and a new deployment on a target platform always requires a new release. At the same time, changing one or more configuration entries in a twelve-factor app also requires a release. A specific release (a release “version”), therefore, always allows an exact conclusion not only to the particular version of the application but also to the precise set of configuration entries.
It is the task of the runtime environment to historical archive releases and to provide information about the exact structure of a release.
A twelve-factor app is built so that it is based on one or more stateless processes at runtime.
Stateless means that the runtime environment can terminate an application process and replace it with a new strategy. The runtime environment can also run not just one but several methods of an application in parallel if it decides that this makes sense, for example, for performance or reliability reasons.
For the application itself, data that is permanently required must always be stored in backing services (see Section 4 ). The application can, of course, use any existing local file system or memory, for example, for local caching operations, but must not make any assumptions about the availability of this data. For example, the runtime environment could clean up a file system that was not explicitly made available as a backing service at any time and delete data stored there.
The application can (and should) divide processes into different process types. For example, a typical web application would have the “Web” process as a process type. This process receives requests from HTTP clients, processes them, and sends the result back to the client.
In parallel, however, this application could define another process type that performs background operations that are not directly related to HTTP client processing. Such background operations (commonly called “workers”) could be, for example, updating search indexes or sending messages regularly at specific times.
Different applications can define different process types here, depending on the exact area of application.
7. Port binding
A twelve-factor app’s communication with its outside world always occurs through one or more ports managed and preserved by the runtime environment. A typical web application will typically use (at least) ports 80 (for unencrypted HTTP requests) and 443 (for encrypted HTTP requests).
Using dedicated environment variables as part of the configuration (see section 3 ), the application is informed by the runtime environment in which ports are available for communication between the application and the outside world. Binding these ports allow the application to process incoming requests and send appropriate responses.
Such a setup is in contrast to classic Java application servers, for example, where communication has already been taken over by the application server, and the application itself communicates with HTTP clients via the servlet API.
8. Concurrency and Scaling
We have already defined concurrent processes (background processes, so-called workers) in Section 6. An application’s scaling can happen horizontally and vertically in a twelve-factor app. Each process type is mapped to specific hardware components by the runtime environment. So it is conceivable that each “Web” type process runs on its own (virtual or real) machine with a specific CPU and memory configuration.
Scaled vertically, better CPU and/or memory equipment can now be used here. Scaled out, the runtime environment could decide to start and run the “web” process not just once but multiple times on separate machines.
Since the Twelve-Factor app is designed according to Section 6 to be executed within a clearly defined process, the runtime environment has a high degree of freedom here to make the best possible use of existing hardware resources and to provision new resources if necessary.
Different process types can be scaled differently. For example, while a background process can only be run cheaply once on a machine with few resources, for the main process type that has to process a large number of HTTP requests at the same time, we might want to use several powerful machines on which the individual processes can be placed.
9. Throwaway Processes
As we already learned in section 4 and section 6, a twelve-factor app is designed to always (only) store persistent data in the backing services. The actual application itself must not hold any state in a running process and must not make any assumptions about the availability of such a state.
From this, it follows that a single process can be regarded as a “disposable process”. It is created by the runtime environment at a certain point in time, fulfils certain tasks during its lifetime, e.g. processing requests via a port (see Section 7 ) or processing background actions, and can then (or even during) be “cleaned up” by the runtime environment” will.
An application should therefore be designed so that restarting and terminating are not exceptions but normal operations that occur repeatedly.
Two design paradigms for an application are, therefore, fast start, so that a new process can process data immediately, and clean shutdown, so that the resources of a terminated process are quickly available again for other procedures.
10. The similarity between development and production systems
The more different a test or development system is from the existing production system on which the application runs, the more difficult, complex and expensive the development process becomes. If when a defect is reported in the production system, it is not possible to find out immediately why and under what conditions the error occurs because a developer computer is configured significantly differently (“works on my machine”), not only frustration arises for everyone involved, but real and tangible frustration Costs.
A Twelve-Factor app is optimized by the different principles to keep the differences between other target systems as small as possible. A development system (i.e. a setup of the application on a single developer’s computer) is “just” another environment, which is mainly characterized by the set of environment variables for configuration (see Section 3 ) and a reduced set of active process types (see Section 8 ) Are defined.
At the same time, this setup makes it easy for new developers to use an existing code base and use it for their own tests. Ideally, a separate set of environment variables for configuration and a local runtime environment are sufficient.
Good logs help application developers to get an insight into a running application and “look under the hood”. Not only can errors be proactively analyzed and corrected, but general statements can be made about a running system and predictions about how certain features are used (or not used), confirmed or falsified.
Log files are often still used here, written and managed by each application according to different principles. However, log files contradict the rules defined in Section 8 and Section 9. They contain a status (the log messages) and must persist.
A twelve-factor app only sees log messages as a special type of data that must be persisted by the application and made available to a group of users (e.g. developers or administrators). As for all other data, the correct way is to transfer the log messages to a log aggregator, which is regularly provided as a backing service (see section 4 ) by the runtime environment.
Good log aggregators allow convenient management of log messages over a longer period and are significantly more powerful than simple log files.
For the application, writing log messages should be very simple: each line in a log represents a log message, and each log message is automatically passed to the log aggregator. The easiest way to do this is to write all log messages directly to the two standard streams STDOUT and STDERR and to send all incoming messages on these streams to the log aggregator.
12. Administration Processes
From time to time, almost every application needs specific administration processes. Things that only happen once (or at very different intervals) are not an essential part of the actual application but are still useful and meaningful (“one-off processes”).
Such a process could, for example, be the export of certain information into a CSV file, which is then used by other systems or people.
In a twelve-factor app, such administration processes are “only” a special process type (see section 6 ) and are treated as such. As a result, such “one-time processes” also have access to the same backing services and environment variables as the rest of the “permanent” processes.
Such one-time processes are executed by the runtime environment the same way the other methods are implemented, with the difference that the one-time process is not executed continuously. Instead, a process is created with the associated resources, the actual process command is completed, and then the process is terminated with the resources used.
The Twelve-Factor App Summary
The individual principles of the Twelve-Factor App partly build on each other and support each other. For example, a throwaway process ( Section 9 ) only becomes possible and meaningful through the definition of a process type ( Section 6 ) and the availability of backing services ( Section 4 ).
The major goal of making the development of an application and its operation as simple and “noiseless” as possible is promoted from different sides with different principles and rules. At the same time, however, a “complete expansion stage” is not always required in all situations, in which all regulations are implemented and followed down to the smallest detail.
Many concepts and ideas can also be applied outside of platform-as-a-service environments, for which a twelve-factor app was originally designed, sometimes directly, sometimes “just” as inspiration for your own application architecture.
The ideas of the Twelve-Factor app serve as a powerful tool in every developer’s toolbox and can help us do what we want to do faster and more calmly: to provide our users with useful features.