Build once CI, Deploy multiple times for modern web apps using Docker, Kubernetes and Spinnaker.
By: Mohit Yadav
Tags: Continuous Integration, Continuous Delivery, Web App Development, Technology, DevOps

To illustrate the flow for CI/CD for build once, deploy any times pattern, we can visualize with this diagram.

Diagram Showing Typical Flow for Single CI connected to multiple CD Pipelines.

In the above diagram, as you can see, we have following components

) A Version Control System i.e Github/Gitlab/Bitbucket. ) A CI Pipeline, i.e Jenkins, Travis CI, CircleCI (pre-configured with Docker). ) A Container Registry, i.e Amazon ECR/ Azure Container Registry. ) One or more Continuous Deployment Pipeline depending on your infrastructure, these pipelines should have an agent which is subscribed to the container registry(Spinnaker is recommended if you are using Kubernetes cluster).

To understand this pattern:

Given: we have an app called Lucifer which is running now on version 1.1.0 on staging and production .

Goal: To deploy a new feature Save Lucifer to staging -> QA This feature -> Deploy to production if QA is successful.

Assumptions: Our main branch is master and we want to avoid multiple builds because the unit test + lint take more than 30 minutes, also we want to avoid pushing separate images for staging and production

In order to achieve the above goal, we can take a step back and break the problem into 2 parts

) Build the app. ) Run the app.

Building an app

For our scenario we want to build Lucifer ones, which means when the feature is merged into master branch, we trigger a CI Pipeline which runs tests + linter and code quality checks and on success it builds the docker image tagged as lucifer-v1.2.0 with tag save-lucifer

One famous approach i used in past for CI was:

Make 2 git branches staging and master , here it would mean running the CI Pipeline 2 times and pushing images lucifer-v1.2.0-stg when code is merged to staging and lucifer-v1.2.0-prod when code is merged to master.

While the above approach seems quite intuitive and simple, but the approach with Build ones CI has many advantages over it.

) We always build a feature once even if we have any number of environments such as staging testing or production which saves CI resources and build time for different environments. ) We always have one image per feature in docker registry, hence we save the storage cost and also have it makes the rollback and versioning strategy simpler. ) Also we avoid issues with dependency version management as our docker image will always contain the same build with same version of dependent packages(this is very important as sometimes some packages introduce breaking changes unknowingly which leads to breaking changes if we have separate builds) ) If we build different images per environments then there is no guarantee that images are “similar enough” to verify that they behave in the same manner. It also opens a lot of possibilities for abuse, where developers/operators are sneaking in extra debugging tools in the non-production images creating an even bigger rift between images for different environments.

Moving on, after building lucifer-v1.2.0 , we push it to the docker registry such as Amazon ECR/ Azure Container Registry, where it resides alongside lucifer-v1.1.0

The main motive to do this step is to decouple the CI and CD flow and also to maintain versions of our app in case of rollbacks.

Running/Deploying the app

The next step of our task is to deploy our new feature lucifer-v1.2.0 .

Usually this step should be decoupled from the CI Step and one way to achieve this is to use an agent which is subscribed/polling the Container Registry, if you use Kubernetes then Spinnaker is quite useful.

Basically Spinnaker allows us to define strategies for subscribing to the container registry and define pipelines for different environments.

So in our case we can define a strategy to watch the version changes to lucifer , and 2 pipelines, one for staging and other for production, the staging pipeline will have trigger to deploy to Kubernetes as soon as a new image is published, but for production pipeline, we can put a manual trigger which waits for a QA/Business manager to deploy this feature once it is QA’ed on staging.

So as soon as the container registry receives image lucifer-v1.2.0 the spinnaker agent triggers the staging pipeline and it deploys the app to Kubernetes cluster, where it can be QA’ed, once the QA passes, the business manager can trigger the Production pipeline and the image lucifer-v1.2.0 would be deployed to production in seconds.

This process is quite straightforward and intuitive because of the tools like Spinnaker, Docker, Kubernetes.

In short the pros of this approach is:

) It makes the job easier for DevOps team as they do not have to setup app specific dependencies as they always will be using the dockerized app. ) It reduces the build +deploy time greatly as we are reusing docker images with different environment variables, as build time for big apps are >30 minutes sometimes.

References used:

Docker Anti Patterns