Skip to content

Continuous Deployment

This page documents how we use GitLab CI to deploy new versions of our applications automatically.

Overview

Products generated from the Google Cloud deployment boilerplate are deployed entirely by terraform. Everything about a deployment, including which version of each container image is running, is described in the infrastructure project. Deploying a change means merging a Merge Request into that project.

GitLab CI never changes Cloud Run services directly. Do not deploy by running pipelines with variables set by hand. Older deployments did this using the DEPLOY_ENABLED and WEBAPP_DOCKER_IMAGE variables, and that is no longer supported.

The terraform pipeline

The infrastructure project includes the terraform-pipeline CI template, which adds one stage per workspace: development, staging and production. Each stage has a terraform-plan-{workspace} job and a terraform-apply-{workspace} job.

The apply job applies the plan produced by the plan job in the same pipeline, so what is applied is exactly what was shown in the plan.

Pipeline Plan jobs development apply staging apply production apply
Merge Request Run for every workspace Manual Not created Not created
Default branch or tag Run for every workspace Manual Automatic Manual, after staging

This means that:

  • Reviewers can see the effect of a Merge Request on every environment by reading the output of the plan jobs.
  • An unmerged change can be tried out in development by triggering its apply job from the Merge Request pipeline.
  • staging always matches the default branch.
  • Production is only changed when someone triggers the apply job by hand, and only once staging has been deployed in the same pipeline.

The template also includes linting and security jobs, such as trivy. See Secure Continuous Deployment for why these are required.

Disabling jobs

Setting any of the following CI variables disables the corresponding job:

  • TERRAFORM_PLAN_DEVELOPMENT_DISABLED
  • TERRAFORM_APPLY_DEVELOPMENT_DISABLED
  • TERRAFORM_PLAN_STAGING_DISABLED
  • TERRAFORM_APPLY_STAGING_DISABLED
  • TERRAFORM_PLAN_PRODUCTION_DISABLED
  • TERRAFORM_APPLY_PRODUCTION_DISABLED

Additional workspaces

Products with workspaces beyond development, staging and production can copy an existing pair of terraform-plan-... and terraform-apply-... jobs into their .gitlab-ci.yml and change the workspace name.

Which container image is deployed

The container images for each workspace are set in the container_images local in locals.tf:

locals.tf
locals {
  container_images = merge(
    local.default_container_images,
    lookup({
      production = {
        webapp_tag = "1.2.3"
      }
      staging = {
        webapp_tag = "1.2.3"
      }
    }, terraform.workspace, {})
  )

  default_container_images = {
    webapp_branch = ""
    webapp_tag    = "latest"
  }
}

Workspaces without an explicit entry fall back to default_container_images, which deploys the image tagged latest. Pin staging and production to a released version so that every deployment is a reviewed change to the infrastructure project.

Releasing a new version

With release automation enabled in the webapp project and renovatebot configured to track releases in the infrastructure project, a release flows through to production as follows:

  1. Merging the release Merge Request in the webapp project creates a tag. CI builds a container image tagged with the version number and pushes it to the product's Artifact Registry.
  2. Renovatebot opens a Merge Request in the infrastructure project updating webapp_tag.
  3. The plan jobs in that Merge Request show the change to each environment.
  4. Merging the Merge Request deploys the new version to staging.
  5. Triggering the terraform-apply-production job deploys it to production.

See also