git branching strategies for infrastructure code

October 28, 2021

When developing infrastructure code for larger organizations, I typically find that the source code repositories where we put our code breaks down into two main categories, ‘module’ repositories and ‘config’ repositories. These two repository types work together to declaratively define the current state of deployed infrastructure.

In this blog post I’ll discuss why you should use a git flow based workflow, with a CHANGELOG and semantic versioning with module repositories, while using a trunk-based workflow without versions or a CHANGELOG for config repositories.

Module Repositories

Module repositories store reusable infrastructure components, similar to shared libraries in application development. In practice this could be literally a Terraform module, or a collection of CloudFormation templates that define a particular architecture component. These modules are parameterized so they can be reused in different environments and situations. They don’t have specifics for a particular environment - they are instead meant to be instantiated again and again via a config repository.

Examples of infrastructure components that could be defined by one module repository could be a VPC, a shared RDS database, or ECS service definitions for a particular type of application workload.

Our Terraform module for provisioning a static website with a CICD pipeline is an example of a module repository.

Config Repositories

Config repositories, as their name implies, store information about the current configuration of deployed resources. These repositories are typically organized with a folder structure that represents a logical hierarchy of deployed workloads. For example, a config repository could have a structure similar to the following:

~/src/oe/internal/aws-infra$ tree
.
├── README.md
└── terraform
    └── accounts
        ├── client-sandbox-1-727272727272
        │   └── us-east-1
        │       └── vpcs
        │           └── vpc1
        │               └── main.tf
        ├── main.tf
        ├── oe-patterns-dev-959595959595
        │   └── global
        │       └── main.tf
        ├── oe-patterns-prod-878787878787
        │   └── global
        │       └── main.tf
        └── oe-prod-404040404040
            ├── us-west-1
            │   └── jitsi
            │       └── main.tf
            └── us-west-2
                ├── distributed-load-testing
                │   └── main.tf
                └── remote-access-vpn
                    └── main.tf

In this example we are using Terraform and Terraform Cloud.

Each directory that has a main.tf represents a distinct Terraform Cloud Workspace with its own deployment pipeline and lifecycle.

The main.tf files in these directories typically only configure providers and instantiate one or more modules (from the module repositories). For example, the aws-infra/terraform/accounts/oe-prod-440643590597/us-west-2/distributed-load-testing/main.tf could look like:

terraform {
  backend "s3" {
    bucket = "oe-prod-tf-state-us-west-2"
    key    = "accounts/oe-prod-404040404040/us-west-2/distributed-load-testing/terraform.tfstate"
    region = "us-west-2"
  }
}

provider "aws" {
  region  = "us-west-2"
  version = "~> 2.27"
}

module "distributed-load-testing" {
  source  = "ordinaryexperts/distributed-load-testing/aws"
  version = "0.1.0"

  admin_name = "dylan"
  admin_email = "dylan@ordinaryexperts.com"
}

The important point to note is that only configuration and instantiation of modules is happening in the config repository.

Workflows for Module and Config Repositories

So now that we have an understanding of what module and config repositories are used for, why would we want to use different git workflows for them?

Module repositories, like application libraries, should have regular releases of new versions. Because of this, it makes sense to use a git workflow that supports releases and versioning. The git flow, semantic versioning, and CHANGELOG.md combo that we typically use is a good fit for this use case and is also well known by experienced developers.

Config repositories should instead use a truck-based workflow. This is because the config repository is capturing the current state of the system, and also is limited to configuration updates. This means that there shouldn’t be much actual development going on in the config repositories, just updates to the desired state of the world, which are then deployed via the automated pipelines.

If feature branches, or even a develop branch were used in a config repository, it would quickly lead to confusion as to which branch represented the intended state of the infrastructure. It doesn’t make sense to do releases and tags of configuration repositories, as the git history itself (as well as likely some pipeline execution history) is the audit trail of changes and deployments.

It also doesn’t make sense to track changes separately in a CHANGELOG.md file for config repositories because the changes are self-describing, and practically having to make entries to CHANGELOG.md in a config repository leads to a lot of git merge conflicts - it isn’t worth it.

So when you are starting new repositories for your infrastructure projects, think about if it is a module or a config repository, and use the appropriate git workflow for the job.

Happy Coding!

Dylan