Implementing GitOps practices will take your software delivery pipelines to the next level. Declarative, immutable, and continuously reconciled infrastructure brings many benefits when managed through GitOps best practices. Over the years, I have helped many development teams build and improve their GitOps workflows. In this blog, I will share four approaches to managing code used in those pipelines.
The "Ops" half of "GitOps" refers to configuration code, or Infrastructure as Code (IaC). Software depends on the resources managed by this code to function. Managing this configuration in Git repositories offers many benefits. Often the structure of this code is an afterthought, which leads to significant refactoring in the future.
Application and Infrastructure Code in One Repository
The first example manages application code and infrastructure code in the same repository. A single long-lived branch exists (main).
Below is a Node.js project with application code in the root, and YAML files in the kubernetes directory. Changes to development.yaml apply to the development environment, changes to production.yaml apply to the production environment.
Infrastructure code and application code in the same repository keeps everything versioned together. There is no need to connect the dots between multiple repositories to reproduce the state of the application and configuration at a certain point in time.
One repository means less context switching for developers. Developers don’t need to change repositories when making changes to infrastructure code.
No privilege separation. Developers with access to the repository will be able to change both application and infrastructure code.
Some organizations require separation between application and infrastructure code. The examples below all manage application and infrastructure code in their own repositories. This improves privilege separation as each Git repository can set its own user privileges.
Separate Infrastructure Repository, Multiple Branches
You may be familiar with Git branching workflows such as Gitflow. Gitflow has fallen out of favor recently as trunk-based development has gained popularity. There are good reasons to avoid more than one long-lived branch in your Git repository. Yet, multiple long-lived branches are still worth considering in certain cases.
Below is a Helm chart repository with two long-lived branches, development, and production. Changes always originate in the development branch. Promotion to production requires merging development into production. The development environment uses the development-values.yaml values file in the development branch. The production environment uses the production-values.yaml values file in the production branch.
Low risk of configuration drift when promoting changes between environments. Merging branches ensures that no changes will be missed.
Improved privilege separation between development and production changes. For example, GitHub supports branch protection rules. The owner of the repository can control which users can commit to a branch.
This is a “one lane road” for your infrastructure code. Changes in the development branch can block production changes (without cherry-picking desired changes).
Separate Infrastructure Repository, Directory-Based
Now, let’s consider a repository where a single long-lived branch exists (main). Each environment has its own directory.
Below is a Terraform repository with separate development and production directories. Changes to development use the development.tfvars tfvars file in the development directory. Changes to production use the production.tfvars tfvars file in the production directory.
Changes made in the development directory do not affect the production directory.
Increased risk of configuration drift between environments. There is a high burden on the developer to understand differences between directories.
No privilege separation. Users can make changes to both development and production environments.
Multiple Infrastructure Repositories, One per Environment
Let’s consider an approach where each environment has its own dedicated repository. Each repository has a single long-lived branch (main).
Here is an example Terraform project, where development and production are separate repositories. Changes to development use the development.tfvars tfvars file in the development repository. Changes to production use the production.tfvars tfvars file in the production repository.
Highest level of privilege separation. Any feature that your Git host provides around user/group access at the repository level will be available to you. Only users that need to make changes to production will be able to commit changes to the production repository.
Easier to bring up new environments, or migrate existing environments. When bringing up a new environment, create a new repository. There is no need to integrate with an existing repository to bring up a new environment.
Higher risk for configuration drift between environments. There is a high burden on the developer to understand differences between repositories.
In my experience, one repository per environment is the most future-proof method for managing your GitOps code. The privilege and environment separation benefits outweigh the potential drawbacks. If you decide the separation is not required in the future, you can collapse multiple repositories into one. The good news is that whatever method you choose, Harness’ suite of products supports them all.
Whether you are building, testing, and publishing artifacts with Harness CI, deploying with Harness CD, or taking your pipelines to the next level with Harness GitOps (currently in beta), we’ve got you covered! Also, every Harness pipeline can take advantage of advanced features around governance, chaos engineering, and more.