The twelve-factor app methodology provides the guidelines for building scalable, maintainable, and portable applications by adopting key characteristics such as immutability, ephemerality, declarative configuration, and automation. Incorporating these characteristics and avoiding common anti-patterns will help us build loosely coupled and self-contained microservices. Implementing these guidelines will help us build cloud-native applications that are independently deployable and scalable. In most cases, failed attempts at creating microservices are not due to complex design or code flaws – they have set the fundamentals wrong from the start by ignoring the widely accepted methodologies. The rest of this section will focus on the 12 factors in light of microservices to help you learn and adopt the principles so that you can implement them successfully.
Code base
The twelve-factor app methodology emphasizes every application having a single code base that is tracked in version control. The application code base may have multiple branches, but you should avoid forking these branches as different repositories. In the context of microservices, each microservice represents an application. These microservices may have different versions deployed across different environments (development, staging, or production) from the same code base. A violation of this principle is having multiple applications in a single repository to either facilitate code sharing or commits that apply to multiple applications, which reduces our ability to decouple them in the long run. A better approach is to refactor and isolate the shared piece of code as a separate library or a microservice. The following diagram shows the collaboration of different developers on a single code base for delivering microservices:
Figure 1.3 – Different developers working together on a single code base
In the previous diagram, a microservice team is working on a single code base to make changes. Once these changes are merged, they are built and released for deployment to different environments (staging and production). In practice, staging may be running a different version than production, although both releases can be tracked from the same version control.
Dependencies
In the Twelve-Factor App, the dependencies should be isolated and explicitly declared via a dependency declaration manifest. The application should not depend on the host to have any of its dependencies. The application and all its dependencies are carried together for every deployment. For example, a common way of declaring Node.js application dependencies is package.json
, for Java applications, it's pom.xml
, and for .NET Core, it's .csproj
. For containerized environments, you can bundle the application with its dependencies using Docker for deployment across environments. The following diagram depicts how application dependencies are managed outside the application's repository and included later as part of container packaging:
Figure 1.4 – Application dependency isolation and its inclusion via a Docker file
Containers help in deploying applications to different environments without the need to worry about installing application dependencies. npm is a package manager that's responsible for managing packages that are local dependencies for a project. These packages are then included as part of container packaging to build container images.
Config
A single code base allows you to deploy your application consistently across different environments. Any application configuration that changes between environments and is saved as constants in the code base should be managed with environment variables. This approach provides the flexibility to scale an application with different configurations as they grow. Connection strings, API keys, tokens, external service URLs, hostnames, IP addresses, and ports are good candidates for configs. Defining config files or managing configs using grouping (development, staging, or production) is error-prone and should be avoided. You should also avoid hard-coding configuration values as constants in your code base. For example, if you are deploying your application in Azure App Service, you can specify configuration values in the Azure App Service instance rather than inside the application's configuration file. The following diagram demonstrates deploying a container image to different environments with different configs:
Figure 1.5 – Application deployed to different environments with different configs
A container image represents the template of a microservice and is compromised of application code, binaries, and dependencies. The container image is then deployed to the production and staging environments, along with their configurations, to make sure that the same application is running across all environments.
Backing service
A backing service is a service dependency that an application consumes over a network for its normal operation. These services should be treated as attachable resources. Local services and remote third-party services should be treated the same. Examples include datastores (such as Azure SQL or Cosmos DB), messaging (Azure Service Bus or Kafka), and caching systems (Azure Redis Cache). These services can be swapped by changing the URL or locator/credential in the config, without the need to make any changes to the application's code base. The following diagram illustrates how different backing services can be associated with a microservice:
Figure 1.6 – How an application can access and replace backing services by changing access URLs
This diagram depicts a running instance of a microservice hosted inside a container. The microservice is consuming different services that are externalized using parameters. Externalizing backing services helps microservices easily replace backing services.
Build, release, and run
The deployment process for each application should be executed in three discrete stages (build, release, and run), as follows:
- The build stage compiles a particular version of the code base and its assets, and then fetches vendor dependencies to produce build artifacts.
- The release stage combines the build artifacts with the config to produce a release for an environment.
- The run stage runs the release in an executing environment by provisioning processes, containers, or services.
As a general practice, it's recommended to build once and deploy the same build artifact to multiple environments. The following diagram demonstrates the process of building and releasing microservices that are ready to be run in different environments:
Figure 1.7 – Creating a release for deployment on an environment
Processes
The process is an execution environment that hosts applications. These processes are stateless in nature and share nothing with other processes or hosts. Twelve-factor app uses stateful backing services such as datastores to persist data to make it available across processes or in the case of process failure. Sticky sessions, or storing data in memory or disk that can be reused for subsequent requests, is an anti-pattern. Session states should be externalized either to a database, cache, or any other service. Externalizing the state to a host would be an exception and considered an anti-pattern as it significantly impacts the ability of these processes to run on individual hosts, and it also violates the scalability guidelines of the twelve-factor app methodology.
An example of a stateless process and its stateful backing services, such as Azure Cosmos DB and Azure Redis Cache, is shown in Figure 1.6.
Port binding
Traditionally, web applications are deployed inside web servers such as IIS, Tomcat, and httpd to expose functionality to end users. A twelve-factor web app is a self-contained application that doesn't rely on running instances of a web server. The app should include web server libraries as dependencies that are packaged as build artifacts for deployment. The twelve-factor web app exposes HTTP as a service binding on a port for consumption. The port should be provided by the environment to the application. This is true for any server that hosts applications and any protocol that supports port binding. Port binding enables different apps to collaborate by referring to each other with URLs.
Concurrency
The loosely coupled, self-contained, and share-nothing characteristics of the twelve-factor app methodology allow it to scale horizontally. Horizontal scaling enables adding more servers and spawning more processes with less effort to achieve scalability at ease. The microservice architecture promotes building small single-purpose and diversified services, which provides the flexibility required to scale these services to achieve better density and scalability. There are various managed services available on Azure that provide you with a better capability for configuring auto-scaling based on various metrics. The following diagram demonstrates the scalability patterns of different microservices:
Figure 1.8 – Scalability of different microservices
At a given instance, a different number of microservice instances are running to meet the user's need. The orchestration platform is responsible for scaling these microservices to support different consumption patterns, which helps with running microservices in an optimized environment.
Disposability
The twelve-factor app methodology should have a quick startup, a graceful shutdown, and be resilient to failure. Having a quick startup time helps with spawning new processes quickly to address spikes in demand. It also helps with moving processes across hosts and replacing failed processes with new ones. A graceful shutdown should be implemented with SIGTERM to allow a process to stop listening to new requests and release any resources before exit. In the case of long-running processes, it's important to make sure that operations are idempotent and can be placed back on the queue for processing. Failures are bound to happen; what's important is how you react to those failures. Both graceful shutdowns and quick startups are important aspects of achieving resilience.
Dev/prod parity
"But it works on my machine."
– Every developer
The twelve-factor app methodology advocates having parity between development, production, and any environment in-between. Dev/prod parity aligns with the shift-left strategy, where issues are identified at an earlier stage to allow more time to resolve them with the least impact. It also helps with debugging and reproducing issues in the development and staging environments. A common practice is to use different database versions in development and production environments; this is a violation. In a containerized environment, make sure that you are using the same container image across environments to maintain parity. It's important to have the capability to recreate the production environment with the least amount of effort to achieve parity, reproducibility, and disposability. DevOps practices are crucial for achieving dev/prod parity, where the dev and ops teams work together to release features with continuous deployment. They use the same set of tools in all environments to observe their behavior to find issues.
Logs
The twelve-factor app methodology treats logs as event streams. It never concerns itself with how these events are routed, processed, or stored. In a traditional monolithic application, logging libraries are used to allow applications to produce log files. Cloud-native development differs from traditional development, where the application is built as a microservice. A single request may be processed by multiple microservices before it can produce a meaningful result. Monitoring and tracking the collaboration is important to provide observability of the overall system. The distributed nature of the system makes logging a daunting task that should be handled in a unified manner to support consistency and scalability. The twelve-factor app (microservice) doesn't contain a logging library as its dependency; instead, it uses a logging agent as a separate process to stream logs. For example, these events can be sent to Azure Event Hub for long-term archival and automation, or Application Insights for monitoring or raising alerts.
Admin processes
Design admin/management processes with the same rigor that you use to develop and run any other application. It should run in the same environment alongside other processes while following the same practices discussed earlier as part of the twelve-factor app methodology. Admin processes should be maintained inside the code base, with the config released to environments using the build, release, and run practice. Database migration, running ad hoc scripts, and purging data are all examples of admin processes.
The twelve-factor app is one of the most widely adopted methodologies for building cloud-native applications, and it aims to help architects and developers follow the procedures that are aligned with best practices and standards, especially while building microservices. In the next section, we will explore a few additional factors that are essential for building cloud-native applications.