by Rodrigo Rodríguez, Software Architect at Growth Acceleration Partners
Coding for the cloud is not for the faint-hearted. It’s a high-stakes game that demands a whole new playbook. To hit the jackpot, you need to learn the game’s rules and master the right strategies. Discover the secrets to unlocking limitless possibilities in cloud development. And see how you can learn how to streamline your efforts to create robust and seamless applications that will take the digital world by storm.
At GAP, we have worked on cloud-oriented applications for a long time, but struggled to determine the “best way” to create cloud applications. We tried different possibilities until we found the 12-factor app principles, which fit like a glove.
This post delves into the 12-factor app principles, the key to successfully building cloud-based applications. With these principles, your applications can scale rapidly, adapt to market changes, and deliver unparalleled performance in the cloud.
The Principles
I. Codebase
One codebase tracked in revision control, many deploys
Applications have a single codebase that produces any number of immutable releases destined for different environments.
How to apply it?
- Each application should have its code repository.
- Use independent CI/CD pipelines for each application/library.
- Shared libraries should have separate repositories and build processes.
- Follow a robust Git workflow with PRs and approval process.
- Implement automated tests with good coverage for application functionality.
II. Dependencies
Explicitly declare and isolate dependencies
A 12-factor app never relies on the implicit existence of system-wide packages. Instead, it declares all dependencies, completely and precisely, via a dependency declaration manifest.
How to apply it?
- Use a dependency manager to configure project dependencies (e.g., Composer, NPM).
- Avoid relying on system-wide packages on servers/instances.
- Include dependencies in the configuration or Infrastructure-as-Code (IaC) process.
- Containers should have all required dependencies.
- Declare dependencies with specific versions and tolerances.
- Design services to be loosely coupled and declaratively specify dependencies.
III. Config
Store config in the environment
Credits: 12factor.net
A 12-factor application requires strict separation of config from code. Config varies across deployments. The recommendation is to have all configurable items in environment variables, or to use an external centralized tool that could allow the management of values without any code change.
How to apply it?
- Configure external services using environment variables or external tools like Hashicorp Consul.
- Avoid hard-coding URLs and credentials in the code.
- If using a configuration file, automatically generate it during the CI/CD process.
- Utilize DNS-based service discovery.
IV. Backing Services
Treat backing services as attached resources
Local and third-party services are treated as interchangeable resources accessed via a URL or stored credentials in the app’s config, allowing seamless swapping without code changes.
How to apply it?
- Use object storage where files are needed (instead of local storage).
- Use external databases to persist state.
- Use environment variables for configuration.
- Use configurable timeouts on connections and responses from backends.
V. Build, Release, Run
Strictly separate build and run stages
In the context of a 12-factor app, the codebase undergoes a transformation process consisting of three stages: build where the code is compiled; the release that combines the build with the configuration of the deployment; and the run state that launches the app processes against the release in the execution environment. A 12-factor app strictly separates the different stages.
How to apply it?
- Implement a well-defined process for building the application.
- When using containers, define the ENTRYPOINT in the Dockerfile to run the application.
- Have a robust CI/CD pipeline to generate builds and prepare for release.
- Each release should have a unique identifier, ideally using Semantic Versioning or a timestamp.
- Builds should be immutable, and an automatic registry should facilitate rollbacks and roll-forwards.
- Use a separate release process to deploy application artifacts to different environments.
- Run your application as one or more stateless processes that can be easily scaled up or down.
VI. Processes
Execute the app as one or more stateless processes
Twelve-factor processes are stateless and share-nothing. Any data that must persist must be stored in a stateful backing service.
How to apply it?
- Ensure all processes/applications are stateless.
- Long-lasting states are external to the application and provided by the backing services.
- Processes in a single application must follow the share-nothing pattern.
- Use process monitoring tools to track performance metrics and detect issues.
- Processes must expose health check endpoints.
- Execution does not require privileged execution.
VII. Port Binding
Export services via port binding
Port binding in a 12-factor app establishes a self-contained communication approach, eliminating reliance on the underlying system for network configuration. By binding to a specific port on the host machine and exposing it externally, the app enables communication with other services or applications without exposing the intricacies of the network setup.
How to apply it?
- The application binds to a specific port on the machine it’s running on.
- The application exposes the port to the “outside” world.
- The port is standardized for its specific type of traffic.
- If using containers, Dockerfile defines a PORT definition.
- Services to listen on a preconfigured bind-address.
- Application to listen on non-privileged ports.
VIII. Concurrency
Scale out via the process model
Concurrency in a 12-factor app focuses on enabling the handling of multiple requests or tasks concurrently to enhance performance and scalability. The process model excels in scaling out, as the share-nothing and horizontally partitionable nature of twelve-factor app processes simplifies and ensures the reliable addition of more concurrency.
How to apply it?
- The application supports parallel execution.
- It only relies on a small pool of persistent database connections.
- Database transactions are used to avoid deadlocks.
- Sticky sessions are not required; requests can be directed to any process.
- The application handles asynchronous processing.
- Failures and errors are handled gracefully.
- Concurrent activity is monitored and logged.
IX. Disposability
Maximize robustness with fast startup and graceful shutdown
Disposability means an application should be designed to start and stop quickly and to handle unexpected shutdowns gracefully.
Disposability can be achieved through several practices, such as:
- Maximizing portability
- Minimizing startup time
- Gracefully handling shutdowns
- Supporting stateless operation
- Automating deployments
How to apply?
- Ensure applications are entirely stateless.
- Ensure processes can be easily created or destroyed without additional steps.
- Ensure the application can recover quickly from failures.
- Ensure the application can easily be replaced or upgraded.
- Processes take a few seconds from the time the launch command is executed until the process is up and ready.
- Ensure there are no linked items that affect startup/stop processes.
X. Dev/Prod parity
Keep development, staging and production as similar as possible
This factor emphasizes maintaining consistency between development, testing and production environments. To achieve this, developers should refrain from relying on ad-hoc solutions or workarounds that can lead to inconsistencies and bugs.
In this aspect, the main differences between a 12-factor app and a traditional app are:
How to apply?
- Automate environment generation using IaC tools whenever possible.
- Utilize IaC to create disposable environments that replicate the production environment.
- Ensure all environments function consistently when configured with the same settings.
- Maintain consistent backing services across different environments (e.g., using the same DB version).
- Set up at least one test environment with the same configuration in load-balanced environments, while ideally simulating the environment through containers and an orchestrator in the development environment.
- Use flags to enable/disable functionality without relying on stage or environment knowledge.
- Avoid using hostnames for the conditional routing logic.
XI. Logs
Treat logs as event streams
The logs factor emphasizes treating logs as a critical component of an application’s architecture. Logs capture and record events and messages within an application, such as errors, warnings and status updates. Also, a robust logging system can facilitate efficient debugging and troubleshooting, and help monitor system health and performance.
How to apply?
- Ensure all applications have a well-defined log structure.
- Ensure all relevant events are logged (verify application log levels).
- Ensure the logs are centralized in a single place that has search capabilities (like Splunk, ELK stack or similar alternatives in your cloud provider).
- Applications do not write logs to disk.
- Ensure real-time monitoring tools are applied to each application and found problems are fixed.
XII. Admin Processes
Run admin/management tasks as one-off processes
The admin processes factor highlights the significance of segregating administrative tasks from the main application codebase. Tasks like database migrations or system backups should be treated as separate processes, independent of the main codebase.
How to apply?
- Use separate processes for administrative tasks.
- Keep administrative tasks in separate codebases.
- Automate administrative tasks.
- Limit administrative access.
- Batch processes are executed as a separate container/instance.
Conclusion
In today’s cloud-first environment, transitioning to the cloud has become essential for many businesses. Following the 12-factor app principles can ease this transition as you define the best practices for building cloud-native applications that can fully exploit the cloud’s capabilities.
However, migrating to a 12-factor application can present challenges for some companies, especially those with legacy systems.
To address these challenges, GAP recommends implementing the 12-factor app principles in a phased manner, starting with the factors that may be the easiest for your company to implement and have the most significant impact on the business. Based on our experience, most of the teams would get good benefits by applying at least the factors 1, 2, 3, 5, 10, and 11. After those are fully implemented, it’s possible to continue implementing the remaining factors until accomplishing the desired state.