Container Ready Applications with Twelve-Factor App and Microservices Architecture
Are your applications container ready? (tl;dr)
Over the past several months, I have helped several teams move to containers, and I have gained some insight into what Container Application Readiness means. Container Application Readiness is made easier when applications are built with microservices architecture and Twelve-Factor App methodology, even if that means refactoring.
Container Native — Start with the end in mind
For applications, Container Native has been defined by Salil Deshpande as:
1. Software that treats the container as the first-class unit of infrastructure (as opposed to, for example, treating the physical machine or the virtual machine as the first-class unit).
2. Software that does not just “happen to work” in, on or around containers, but rather is purposefully designed for containers
To me, purposefully designed for containers means that from the start of the application requirements and design, to implementation and delivery, the container is the atomic unit of compute for the application. In short, engineers know from the beginning that the application will (1) run as a container and (2) run within a container orchestration layer (AWS ECS/Fargate, Kubernetes, etc.). This begs the question, “How do we design applications to be run within containers and container orchestration layers?”
Monoliths vs. Microservices
We have all heard of Microservices Architecture (or at least we should have by now), and how it is a better approach to designing applications than a single-stack monolithic approach.
As a review, applications are built in layers. In a typical application, these layers are:
- Presentation — responsible for handling requests and responses
- Business logic — responsible for enforcing workflow and business logic of the application
- Data access — responsible for reading/writing data from/to data stores
- Application integration — responsible for integration to other backing services
In a monolithic architecture, the application is deployed as a single package or binary, containing all the layers. This seems simple at first, especially for testing and deployment. However, monoliths can be very large and complex which can lead to deployment and performance issues. Change is also difficult to manage in monoliths; regardless of what layer needs to be changed, the entire monolith needs to be touched, redeployed, and tested. This introduces larger regression boundaries and blast radii. Monolithic design also prevents the pace-layering application strategy.
Note: Semi-monolithic architectures exist when the monolithic application layers are separated into multiple packages or binaries, but are not quite built using Microservices Architecture.
Trying to containerize a monolith is usually difficult, and often leads to more complexity. In general, containers should be used to simplify the lives of developers. Adding complexity is counterintuitive; in many cases, monoliths must be refactored to take full advantage of containerization.
Instead of monoliths, microservices architecture (MSA) is preferred. In MSA, the monolithic application is transformed to smaller applications of interconnected services. There are many benefits to MSA, but my favorites are:
- MSA decomposes applications into sets of manageable services, which are much faster to develop, and much easier to understand and maintain.
- MSA enables each service to be developed independently by a team that is focused on that service.
- MSA facilitates adoption of new technologies via application pace layering strategies.
- MSA loosely couples services.
- MSA strives for data isolation that facilitates loose coupling and horizontal scalability.
When considering containers as your units of compute for applications, it makes sense to stick to atomic services, and keep deployment artifacts (images and containers) small. Embracing MSA helps you get there.
Twelve-Factor App Methodology
The Twelve-Factor App methodology is used to produce software-as-a-service (SaaS), the development approach used in most modern applications. It was designed by Adam Wiggins and the development team at Heroku. The important characteristics of Twelve-Factor App are:
- Use of declarative formats for setup automation, to minimize time and cost for new developers joining the project.
- Clean contracts with the underlying operating system, offering maximum portability between execution environments.
- Suitable for deployment on modern cloud platforms, obviating the need for servers and systems administration.
- Minimized divergence between development and production, enabling continuous deployment for maximum agility.
- Ability to scale up without significant changes to tooling, architecture, or development practices.
Breaking Down the Factors
While all twelve factors are important, in my opinion, some are more important to containers and microservices than others.
According to the Dependency factor, applications “explicitly declare and isolate dependencies” and do not rely “on implicit existence of system-wide packages”. Not relying on system tools or packages enables applications to achieve loose coupling, which is desired in MSA. Additionally, declaring and isolating dependencies helps the Concurrency factor achieve horizontal partitioning and scalability.
The Config factor espouses the use of environment variables in lieu of config files or code constants for config data and credentials. This also helps avoid the bad practice of grouping environment variables based on environment. Storing config in the environment allows the environment variables to serve as “granular controls”, each one independent of the others. These controls allow the last mile of CI/CD pipelines to more granularly control application configurations at deployment and runtime. Container runtimes and orchestration layers provide mechanisms for injecting these environment variables when containers are instantiated. The overall effect is decentralizing and decoupling configuration.
According to the Logs factor “a twelve-factor app never concerns itself with routing or storage of its output stream.” For containerized applications, that means that applications log to stdout. Doing so allows the application to be deployed in multiple orchestration layers without having to build special logging in each environment. Instead, these apps rely on unified logging and data-collection layer implementations, such as FluentD, built into the underlying orchestration layers. Reusing free services such as logging, provided by orchestrion layers, is a successful containerization pattern that eliminates the need for team to repeat these common tasks.
In his book, “Beyond the Twelve-Factor App”, Kevin Hoffman provides additional details about the original 12-Factors, driving to a common understanding, that we as a community of practitioners can share to eliminate confusion and communicate more clearly. He then introduces additional factors “that should be considerations for any application that will be running in the cloud.”
Without going too deep into the weeds of MSA and Twelve-Factor App methodology, it’s important to realize that MSA and Twelve-Factor App are complementary. For example, Data Isolation, as part of MSA, facilitates loose coupling and horizontal scalability. This elasticity (fast startup and graceful shutdown) is part of how the Twelve-Factor App Disposability factor leads to application robustness.
MSA prescribes how to design applications with interconnected services, keeping deployable units and regression boundaries from growing too large, and allowing for pace-layering. The Twelve-Factor App methodology prescribes the characteristics of atomic application units (or services) needed to eliminate undifferentiated heavy lifting. This allows applications to take advantage of services provided by deployment environments, such as container orchestration layers, like Kubernetes.
Container-ready applications have small footprints and take advantage of free services offered by orchestration layers such as logging, federated security, and metrics gathering. They are atomic and robust units of compute that can be partitioned for elasticity. Whether applications are designed to be container-native from the start, or refactored to be container-ready, embracing MSA and Twelve-Factor App methodology enables deployment into modern container orchestration layers.
DISCLOSURE STATEMENT: These opinions are those of the author. Unless noted otherwise in this post, Capital One is not affiliated with, nor is it endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are the ownership of their respective owners. This article is © 2018 Capital One.