GitOps
Architecting Multi-Environment Promotions Using Git Branches and Folders
Explore proven patterns for promoting changes from staging to production while maintaining environment-specific overrides and isolation.
In this article
The Evolution of Environment Promotion in GitOps
In traditional infrastructure management, moving a feature from a staging environment to production often involves manual execution of scripts or configuration changes. These manual steps introduce a significant risk of human error and environment drift, where the reality of the live system no longer matches the intended design. GitOps addresses this problem by treating Git as the authoritative source of truth for the desired state of every environment.
The core objective of GitOps promotion is to ensure that a change validated in a staging environment is moved to production in a predictable and repeatable manner. By using a continuous reconciliation loop, an agent inside the cluster monitors the Git repository and automatically applies changes to the infrastructure. This approach eliminates the need for developers to have direct write access to the production cluster, significantly improving security and auditability.
Effective promotion strategies require a clear mental model of how configuration is inherited and overridden. We must balance the need for consistency across environments with the practical requirement for environment-specific differences, such as database endpoints, secret references, and resource limits. Understanding these patterns allows teams to scale their delivery pipelines without increasing the cognitive load on individual engineers.
Promotion in a GitOps workflow is not about moving code between servers, but about advancing the declared state of an environment through a version-controlled audit trail.
Identifying the Promotion Gap
The promotion gap occurs when the tooling used to deploy to staging differs fundamentally from the tooling used for production. When developers use different scripts or manual overrides for each stage, they break the feedback loop that ensures a release is safe. GitOps closes this gap by ensuring the same declarative manifests are used across the entire lifecycle.
By formalizing the promotion process, teams can implement automated gates that check for successful health probes and performance metrics. If a change fails to meet quality standards in staging, the GitOps controller prevents the production manifests from being updated. This proactive safety mechanism reduces the likelihood of outages caused by configuration mismatches.
Structuring Repositories for Environment Isolation
One of the most critical decisions in a GitOps strategy is how to structure your configuration files to represent different environments. There are two primary schools of thought: the directory-per-environment approach and the branch-per-environment approach. Each has significant implications for how changes are reviewed and merged by your engineering team.
The directory-per-environment pattern uses a single branch, usually main, and organizes configurations into separate folders like staging and production. This approach is highly recommended because it avoids the complexity of long-lived feature branches and merge conflicts between environments. It provides a clear view of the state of the entire fleet by simply browsing the file tree in a single Git ref.
Alternatively, branch-per-environment uses a dedicated branch for each stage of the lifecycle, where a merge from the staging branch to the production branch triggers a deployment. While this feels familiar to developers used to GitFlow, it often leads to environment drift when hotfixes are applied directly to production branches. It also makes it difficult to use modern tools that expect a static path to manifests within a single repository context.
The Directory-Based Layout Pattern
In a directory-based layout, common configuration is stored in a base folder, while environment-specific changes are stored in overlays. This structure allows you to reuse the majority of your Kubernetes manifests while still customizing the replicas or image tags for production. It simplifies the promotion process because promoting a change simply involves updating a file path or a version string in a specific subdirectory.
1infrastructure-repo/
2├── apps/
3│ ├── inventory-service/
4│ │ ├── base/ # Common manifests (deployments, services)
5│ │ │ ├── deployment.yaml
6│ │ │ └── kustomization.yaml
7│ │ └── overlays/
8│ │ ├── staging/ # Staging-specific overrides
9│ │ │ ├── kustomization.yaml
10│ │ │ └── patch-replicas.yaml
11│ │ └── production/ # Production-specific overrides
12│ │ ├── kustomization.yaml
13│ │ └── patch-resources.yaml
14└── clusters/ # Cluster-level configurations
15 ├── staging-cluster-01/
16 └── prod-cluster-01/Implementing Overrides with Kustomize
Kustomize has emerged as the standard tool for managing environment-specific overrides in a GitOps workflow without resorting to complex templating engines. It works by taking a base set of resources and applying patches or transformations to them based on the current context. This ensures that the underlying logic of your application deployment remains identical across all stages of the pipeline.
When promoting a service, you typically want to change the container image tag to a version that has been verified in a lower environment. Kustomize allows you to define these image overrides in a clean, declarative way that is easy for automated CI systems to modify. This avoids the risk of manual typos in YAML files that could lead to pulling the wrong image or an unstable development snapshot.
Beyond image tags, Kustomize is used to manage resource constraints like CPU and memory limits, which are usually higher in production than in staging. You can also use it to inject environment-specific secrets or config maps, ensuring your application connects to the correct database instance. This separation of concerns keeps your base manifests generic and reusable across any number of clusters.
Declarative Image Promotion
To promote an image, your CI pipeline should programmatically update the kustomization.yaml file in the production overlay folder. This creates a new commit in the Git repository, which the GitOps controller then detects and applies to the cluster. This creates a clear link between the CI build and the actual deployment state.
1# apps/inventory-service/overlays/production/kustomization.yaml
2apiVersion: kustomize.config.k8s.io/v1beta1
3kind: Kustomization
4
5resources:
6 - ../../base
7
8# Override the image tag for production
9images:
10 - name: internal-registry.io/inventory-api
11 newName: internal-registry.io/inventory-api
12 newTag: v1.4.2-stable
13
14# Apply production-specific resource limits
15patches:
16 - path: patch-resources.yaml
17 target:
18 kind: Deployment
19 name: inventory-apiThe Promotion Pipeline and Automated Gates
A robust GitOps promotion strategy requires a well-defined pipeline that manages the movement of configurations between environments. This pipeline usually starts with a successful build and test phase in the application repository, followed by an automated commit to the staging folder in the configuration repository. Once the staging deployment is verified, the pipeline initiates a pull request to update the production configuration.
Automated gates are essential for reducing the risk of a bad deployment reaching production. These gates can include integration tests, security scans, and even manual approvals if your organization requires a human-in-the-loop for production changes. By utilizing Pull Requests as the primary mechanism for promotion, you gain the benefit of discussion threads and approval logs directly in your version control system.
- Isolation: Use separate namespaces or clusters to prevent staging workloads from impacting production resources.
- Verification: Implement automated health checks that must pass in staging before the promotion PR is created.
- Rollback: Reverting a failed promotion is as simple as reverting the specific commit in the Git repository.
- Observability: Use a GitOps dashboard to track the synchronization status and ensure the cluster matches the repository.
When the promotion PR is merged, the GitOps operator inside the production cluster notices the change in the production directory. It calculates the delta between the current state of the cluster and the new desired state defined in the repository. The operator then applies only the necessary changes to achieve parity, ensuring a minimal-impact update to the live environment.
Handling Promotion with GitHub Actions
Modern CI/CD tools like GitHub Actions can automate the modification of Git repositories during the promotion phase. A common pattern involves a script that uses the kustomize edit command to update image tags and then commits the change using a dedicated service account. This ensures that the promotion process is hands-off and strictly follows the defined workflow.
1#!/bin/bash
2# Move to the production overlay directory
3cd apps/inventory-service/overlays/production
4
5# Update the image tag to the version passed from CI
6kustomize edit set image internal-registry.io/inventory-api=:${NEW_VERSION}
7
8# Commit and push the change to trigger the GitOps sync
9git config user.name "Deployment Bot"
10git config user.email "bot@company.com"
11git add kustomization.yaml
12git commit -m "chore: promote inventory-service to ${NEW_VERSION} in production"
13git push origin main