iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article

The Twelve-Factor App: Fundamentals of Cloud-Native Design

に公開

While reading the following book, I came across the Twelve-Factor App, so I've summarized it here to organize my knowledge.
AWS Container Design & Construction [Authentic] Introduction - Revised and Expanded Edition

What is The Twelve-Factor App?

The Twelve-Factor App is a development methodology for building Software as a Service (SaaS).
Specifically, it aims to create SaaS with the following goals:

  • Use declarative formats for setup automation, to minimize time and cost for new developers joining the project.
  • Have a clean contract with the underlying operating system, offering maximum portability between execution environments.
  • Are suitable for deployment on modern cloud platforms, obviating the need for servers and systems administration.
  • Minimize divergence between development and production, enabling continuous deployment for maximum agility.
  • And can scale up without significant changes to tooling, architecture, or development practices.

In short, it is a methodology for developing applications in the cloud.

The 12 Factors of The Twelve-Factor App

  1. Codebase
  2. Dependencies
  3. Config
  4. Backing services
  5. Build, release, run
  6. Processes
  7. Port binding
  8. Concurrency
  9. Disposability
  10. Dev/prod parity
  11. Logs
  12. Admin processes

Let's dive deeper into each of these below.

1. Codebase

One codebase tracked in revision control, many deploys
A codebase refers to a repository managed by a configuration management system like Subversion or Git.
According to The Twelve-Factor App:

There is always a one-to-one correlation between the codebase and the app.

OK Example
The following example is not an application but a distributed system.
In the case of a distributed system, each component can individually comply with the Twelve-Factor principles.
Two codebases for one deployment
I apologize for the confusing diagram, but the following example illustrates sharing a codebase. This state violates the Twelve-Factor principles.
Common code must be factored out into libraries and included via dependency management tools like Maven.
NG Example

2. Dependencies

Explicitly declare and isolate dependencies
The Twelve-Factor App states:

never relies on implicit existence of system-wide packages.

In other words, no package that is implicitly depended upon should exist.
In Maven, you should use pom.xml, and in Ruby, Bundler, to completely and strictly declare all dependencies.
The benefit is that as long as you have the runtime and a dependency management tool like Maven, setting up the environment for a new developer becomes much simpler.

3. Config

Store config in the environment
This might be common sense in development.
You should store credentials for databases or external services in environment variables, and it is necessary to

strict separation of config from code

Since this is likely common knowledge, I will omit the details.
Please note that configuration files such as property files are part of the code.
The Twelve-Factor App requires isolating configuration all the way to environment variables.

4. Backing services

Treat backing services as attached resources
This indicates that services used by the application over the network (backing services, e.g., MQ, Memcached, DB, etc.) should

make no distinction between local and third-party services

For the application, both should be loosely coupled as attached resources accessible via URLs or credentials held in the configuration.
For example, it means that a situation where the application must be modified because the internal processing of the backend has changed is not acceptable.

5. Build, Release, Run

Strictly separate build, release, and run stages
The Twelve-Factor App defines the build, release, and run stages as follows:

  • Build stage
    A stage that transforms a code repository into an executable bundle. Using a specific version of the committed code, it fetches dependencies, places them in the local environment, and compiles binaries or asset files.
  • Release stage
     Takes the build and combines it with the deployment's current configuration. The resulting release contains both the build and the configuration and is ready for immediate execution in the execution environment.
  • Run stage (runtime)
     Runs the application in the execution environment by starting some set of the application’s processes against a selected release.

I believe this strict separation of these three stages is practiced as a matter of course in modern development methods.
In a project running on AWS ECS using Maven, it would look like this:

  • Build using Maven (Build stage)
  • Create a Docker image and push it to ECR (Release stage)
  • Start via EventBridge or similar triggers (Run stage)
    While this is something we do naturally, I believe this stage separation offers benefits such as:
  • Errors occur at the build stage if the code is broken (preventing failures beforehand)
  • The state of a release can be managed using release IDs or versions

6. Processes

Execute the app as one or more stateless processes

Processes are stateless and share-nothing.

This indicates that any data that needs to be persisted must be stored in a stateful backing service (simply put, a database).
For example, using on-memory storage or the file system as a persistent data store or as a shared asset with other processes, rather than just as a cache for a single short transaction, is a violation.

  • Data may be lost during momentary interruptions, deployments, or configuration changes.
  • It cannot be guaranteed that the state upon restart is correct (e.g., a file might be mid-update).

Furthermore, the Twelve-Factor App considers sticky sessions to be a violation.
Sticky sessions involve caching session data in memory and using that session information when receiving requests from the same client.
The Twelve-Factor principles state that such data should be stored in a data store with an expiration time, such as Memcached or Redis.

7. Port binding

Expose services via port binding
Simply put, it means that if you access a specified port (such as 80 for HTTP or 443 for HTTPS), you can access the service without being aware of the application's execution environment (whether it's Tomcat, Apache Httpd, etc.).
This also seems obvious, but through port binding, an app can also become a backing service for another app.
In other words, it can contribute to the loose coupling between applications.

8. Concurrency

Scale out via the process model
This suggests assigning each type of workload to a process type.
By treating workloads as individual processes, scaling out becomes possible just by increasing the number of processes.
This assumes that the share-nothing, stateless processes described in "6. Processes" have been implemented.

9. Disposability

Maximize robustness with fast startup and graceful shutdown

  • Minimize startup time
    • The app should be ready to receive requests or jobs within a few seconds of startup.
    • For Java, using GraalVM or AppCDS are effective means for speeding up startup.
  • Gracefully shut down when receiving a SIGTERM signal
    • Refuse new requests but shut down only after processing in-progress requests.
    • This implicitly assumes that HTTP requests are short.
    • In Java Spring Boot, Graceful Shutdown can be configured via properties (I plan to summarize its mechanism later).

10. Dev/prod parity

Keep development, staging, and production as similar as possible

Many engineers have likely experienced situations where there is a significant gap between development and production environments, often during feature additions in large-scale waterfall projects.

This divergence isn't just about code differences; it also involves:

Keeping these gaps small is the core of Dev/prod parity.

CI/CD (Continuous Integration/Continuous Delivery) seems to be a methodology aimed at realizing this.

  • CI (Continuous Integration)
    • Automates building and testing, performing frequent integrations into the repository.
  • CD (Continuous Delivery/Deployment)
    • Follows the CI process to maintain a state ready for production deployment at any time (Delivery) or automatically applies changes to the production environment (Deployment).

However, in waterfall development, depending on the scale, while CI might be feasible, CD can be difficult. I believe this approach is better suited for agile development.

11. Logs

Treat logs as event streams

The Twelve-Factor App states:

[an app] never concerns itself with routing or storage of its output stream.

This is the philosophy that the application should not be responsible for writing to or managing log files.
Instead, processes write their event stream, unbuffered, to standard output (stdout).
During development, you can check the standard output from the terminal. In staging or production environments, the streams from the application are sent to a specified destination (for example, AWS CloudWatch).
This allows for real-time monitoring using tail and enables:

  • Detecting specific events
  • Graphing scaling trends (e.g., requests per minute)
  • Defining metrics to trigger alerts

However, note that this cannot be applied to applications that do not have an execution environment in the cloud or a display, such as embedded applications.

12. Admin processes

Run admin tasks as one-off processes

Administrative tasks include:

  • Database migrations
  • Running a console to execute arbitrary code or inspect the app's models against the database
  • Running scripts stored in the repository

These administrative processes are executed against a release and must use the same codebase and configuration as the processes running the application.
In other words, the application code and administrative code should be deployed together.

Discussion