Containerized delivery

The usual way to run applications consists of the following steps:

  1. Provision machines and the corresponding infrastructure resources
  2. Install an operating system
  3. Install system programs and application dependencies
  4. Deploy the application
  5. Maintain the running states of the application

The entire process is tedious and complicated, which is why we usually don't want to do it manually. The configuration management tool, introduced in Chapter 1, Introduction to DevOps, is used to eliminate most of the effort otherwise required in the delivery process. Its modular and code-based configuration design works well until application stacks grow complex and diverse. Maintaining a large configuration base is hard, especially if it's a legacy configuration that contains various hacks. Although changing configuration codes with the configuration management tool has a direct impact on the production environment, the configuration code often gets less attention than application code. Whenever we want to update an installed package, we would have to work in entangled and fragile dependencies between the system and application packages. It's not uncommon that some applications break inadvertently after upgrading an unrelated package. Moreover, upgrading the configuration management tool itself is also a challenging task.

In order to overcome this problem, immutable deployments with pre-baked VM images were introduced. This means that whenever we carry out any updates on the system or application packages, we would build a full VM image against the change and deploy it accordingly. This reduces some of the complexity, because we can test changes prior to roll-outs and we're able to customize runtimes for applications that can't share the same environments. Nevertheless, carrying out immutable deployment with VM images is costly. The overhead of booting, distributing, and running a bloated VM image is significantly larger than deploying packages.

The container, here, is a jigsaw piece that snugly fits the deployment needs. A manifestation of a container can be managed within VCS and built into a blob image, and the image can be deployed immutably as well. This enables developers to abstract from actual resources and infrastructure engineers to avoid dependency hell. Besides, since we only need to pack up the application itself and its dependent libraries, its image size would be significantly smaller than a VM's. Consequently, distributing a container image is more economical than distributing a VM image. Additionally, we already know that running a process inside a container is basically identical to running it on its Linux host and, as such, almost no overhead will be produced. To summarize, a container is lightweight, self-contained, and almost immutable. This provides clear borders to distinguish responsibilities between applications and infrastructure.

Due to the fact that Linux containers share the same kernel, there are still potential security risks for the kernel from containers running on top of it. An emerging trend to address this concern is making running VMs as easy and efficient as operating system containers, such as  unikernel-based solutions or the Kata container. Another approach is inserting a mediator layer between applications and the host kernel, such as gVisor.