Lecture
Building applications that run at web scale is hard work. Risks are everywhere - from an application going down due to network overload, to a competitor grabbing your market share because they ship more code and reach demanding users faster than you can. Any advantage you can gain toward building working code at web scale better and faster works in your favor.
Fortunately, the challenges of operating at web scale are well understood. There are solutions. One of them is the «12 Factor App», published in 2011 by Adam Wiggins. The «12 Factor App» is a set of principles that describe a way of building software which, when followed, allows companies to create code that can be reliably released, quickly scaled, and maintained in a consistent and predictable manner.
Below is a brief summary of the principles behind the «12 Factor App».
One codebase tracked in version control, many deploys
The codebase principle states that all assets related to the application, from source code to the initialization script and configuration settings, are stored in a source code repository that is accessible to developers, testers, and system administration staff. The source code repository is also accessible to all automation scripts that are part of the continuous integration / continuous delivery (CI / CD) processes which form part of the enterprise software development lifecycle (SDLC).
Explicitly declare and isolate dependencies
The dependencies principle states that only code that is unique and relevant to the purpose of the application is stored in version control. External artifacts, such as Node.js packages, Java .jar files, or .NET DLLs, should be referenced in a dependency manifest that is loaded into memory during development, testing, and the production runtime environment. You do not want to store artifacts alongside the source code in the source code repository.
Store configuration in the environment
The configuration principle states that configuration information is injected into the runtime environment as environment variables or as settings defined in an independent configuration file. While in some cases it is acceptable to store default settings that can be overridden directly in the code, settings such as the port number, dependency URLs, and state parameters like DEBUG should exist separately and be applied at deployment. Good examples of external configuration files are a Java properties file, a Kubernetes manifest file, or a docker-compose.yml file.
The benefit of storing configuration settings separately from the application logic is that you can apply configuration settings according to the deployment path. For example, you might have one set of configuration settings for a deployment intended for the testing environment, and another set for a deployment intended for the production environment.
Treat backing services as attached resources
The backing services principle encourages architects to treat external components such as databases, mail servers, message brokers, and independent services that can be provisioned and maintained by systems staff as attached resources. Treating resources as backing services promotes flexibility and efficiency in the software development lifecycle (SDLC).
Strictly separate the build and run stages
The build, release, and run principle breaks the deployment process into three reproducible stages that can be created at any time. In the build stage, code is retrieved from source control and built / compiled into artifacts stored in an artifact repository, such as Docker Hub or a Maven repository. Once the code has been built, configuration settings are applied in the release stage. Then, in the Run stage, the runtime environment is scripted, provisioned by means of a tool such as Ansible. The application and its dependencies are deployed into the newly provisioned runtime environment.
The key to build, release, and run is that this process is entirely ephemeral. If anything in the pipeline is destroyed, all artifacts and environments can be rebuilt from scratch using the assets stored in the source code repository.
Run the application as one or more stateless processes
The processes principle, which could more accurately be called stateless processes, states that an application built according to the 12 Factor App structure will run as a set of stateless processes. This means that no process keeps track of the state of another process, and that no process tracks information such as session or workflow state. A stateless process makes scaling easier. When a process is stateless, instances can be added and removed to handle a particular load at a particular point in time. Because each process runs independently, the absence of state prevents unexpected side effects.
Export services via port binding
The port binding principle states that a service or application is identified on the network by a port number rather than by a domain name. The reason is that domain names and their associated IP addresses can be assigned on the fly through manual manipulation and automatic service discovery mechanisms. Thus, using them as a reference point is unreliable. Exposing a service or application to the network by port number, however, is more reliable and easier to manage. At the very least, potential problems arising from a conflict between assigning a port number privately for the network and the public use of that same port number by another process publicly can be avoided using port forwarding.
The core idea behind the port binding principle is that consistent use of a port number is the best way to expose a process on the network. For example, patterns have emerged in which port 80 is standard for web servers running under HTTP, port 443 is the default port number for HTTPS, port 22 is for SSH, port 3306 is the default port for MySQL, and port 27017 is the default port for MongoDB.
Scale out via the process model
The concurrency principle recommends organizing processes according to their purpose, then separating those processes so they can be scaled up and down as needed. As shown in the figure above, the application is accessible on the network through web servers that run behind a load balancer. The group of web servers behind the load balancer, in turn, uses the business logic that resides in the Business Service processes, which run behind their own load balancer. If the load on the web servers increases, that group can be scaled in isolation to meet current demand. If, however, a bottleneck arises due to the load placed on the business service, that tier can be scaled independently.
Supporting concurrency means that different parts of the application can be scaled according to the needs at hand. Otherwise, when concurrency is not supported, architectures have no choice but to scale the application in its entirety.
Maximize robustness with fast startup and graceful shutdown
The Disposability principle states that applications should start up and shut down gracefully. This means performing all necessary «cleanup» before the application becomes available to consumers. For example, a graceful startup ensures that all database connections and access to other network resources are operational. In addition, any other necessary setup work has been completed.
As for shutdown, disposability supports properly closing all database connections and other network resources and logging all shutdown operations, as shown in the code example above.
Keep development, staging, and production as similar as possible
The Dev / Prod Parity principle means that all deployment paths are similar but independent, and that a deployment does not make «jumps» to a different deployment target.
The figure above shows two versions of the application code. Version V1 is intended for release into the production environment. The new version V2 is intended for the development environment. Both V1 and V2 follow the same deployment path: from build to release and then run. If code version V2 is deemed ready for production, the artifacts and settings relating to version 2 will NOT be copied into the production environment.
Rather, the CI / CD process will be adjusted to set the deployment target of V2 to Production. The CI / CD process will follow the expected build, release, and run pattern toward this new target.
As you can see, Dev / Prod Parity is very similar to Build, Release, and Run. The important distinction is that Dev / Prod Parity provides the same deployment process for the production environment as for development.
Treat logs as event streams
The logs principle supports sending log data in a stream that various interested consumers can access. The process of routing log data should be decoupled from the processing of log data. For example, one consumer might be interested only in error data, while another consumer is interested in request / response data. Yet another consumer might be interested in storing all log data to archive events. An added benefit is that even if the application dies, the log data persists afterward.
Run administrative / management tasks as one-off processes
The admin processes principle states that administrative processes are first-class citizens in the software development lifecycle and should be treated as such. The figure above shows a service named Orders, deployed as a Docker container. In addition, there is an admin service named dataSeeder, which can feed data into the Orders service. The dataSeeder service is an administrative process intended for use with Orders, as shown in the diagram below.
However, even though dataSeeder is an administrative process, it is given a build, release, and run deployment path similar to the Orders service. It is also released according to the Codebase and Dev / Prod Parity principles. The dataSeeder admin process is not separate from the overall SDLC, but rather is part of it.
The 12 Factor App principles are designed to allow developers to build applications intended to run at web scale. At first they may seem overwhelming, and in many ways they are. Having to rethink the very essence of how you build software can be a daunting task.
Fortunately, implementing the principles of the «12 Factor App» is not an all-or-nothing proposition. You can adopt them in small, digestible pieces, starting with the first and then gradually moving on to the rest. The trick is to commit to following the principles and then take the first step.
Comments