Skip to content
Codeloom
DevOps

GitOps Branching Strategies for Kubernetes

Design effective Git repository structures and branching strategies for GitOps-driven Kubernetes deployments with Flux and Argo CD.

·5 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • Monorepo vs multi-repo layouts for GitOps
  • Branch-per-environment vs directory-per-environment
  • Promotion workflows across staging and production
  • Practical Flux and Argo CD configuration examples
  • How to handle secrets and environment-specific overrides

Prerequisites

None — this post is self-contained.

GitOps uses Git as the single source of truth for infrastructure and application configuration. A GitOps operator like Flux or Argo CD watches a repository and reconciles the cluster state to match what is in Git. The principle is simple, but the repository structure and branching strategy you choose determines whether your workflow scales cleanly or becomes a merge conflict nightmare.

Repository Layout Decisions

The first decision is how many repositories to use.

Monorepo

One repository contains all Kubernetes manifests for all services and environments:

infrastructure/
  base/
    api/
      deployment.yaml
      service.yaml
      kustomization.yaml
    worker/
      deployment.yaml
      service.yaml
      kustomization.yaml
  environments/
    staging/
      kustomization.yaml
      patches/
    production/
      kustomization.yaml
      patches/

Advantages: atomic changes across services, simple CI/CD, easy to search and refactor. Disadvantages: access control is harder (everyone can see production config), large diffs, and the blast radius of a bad merge is wider.

Multi-Repo

Separate repositories for application code and infrastructure config, or one repo per team:

# Repo: app-api (application code + Dockerfile)
# Repo: app-worker (application code + Dockerfile)
# Repo: infra-config (all Kubernetes manifests)
# or
# Repo: team-alpha-infra (manifests for team alpha's services)

Advantages: fine-grained access control, smaller blast radius, independent release cycles. Disadvantages: cross-service changes require coordinating multiple PRs, harder to maintain consistency.

Most teams start with a monorepo and split when access control or scale demands it.

Branch-Per-Environment vs Directory-Per-Environment

Branch-Per-Environment

Each Git branch represents an environment. The staging branch deploys to staging, the production branch deploys to production:

main ---------> staging cluster
production ---> production cluster

Promotion means merging or cherry-picking from staging to production. This seems intuitive but creates problems:

  • Merge conflicts between branches that diverge over time
  • No single view of all environments in one place
  • Hard to compare staging and production configurations
  • Cherry-picking is error-prone and tedious

Avoid this pattern. It works for small setups but breaks down quickly.

Use a single branch (main) with directories representing environments. Kustomize overlays handle environment-specific differences:

base/
  api/
    deployment.yaml
    service.yaml
    kustomization.yaml
environments/
  staging/
    kustomization.yaml
  production/
    kustomization.yaml

The base contains the common configuration. Each environment directory contains a Kustomization that references the base and applies patches:

# environments/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base/api
patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: api
      spec:
        replicas: 2
images:
  - name: myregistry/api
    newTag: v1.5.0-rc.1
# environments/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base/api
patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: api
      spec:
        replicas: 5
images:
  - name: myregistry/api
    newTag: v1.4.2

Promotion is changing the image tag in the production kustomization. One PR, one diff, one review.

Configuring Flux

Flux watches specific paths in a Git repository. Configure a Kustomization resource for each environment:

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: infra
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/myorg/infra-config
  ref:
    branch: main
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: staging
  namespace: flux-system
spec:
  interval: 5m
  sourceRef:
    kind: GitRepository
    name: infra
  path: ./environments/staging
  prune: true
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: api
      namespace: default

Flux reconciles the ./environments/staging path every 5 minutes. When a PR merges that changes the staging image tag, Flux detects the change and rolls out the update.

Configuring Argo CD

Argo CD uses Application resources to map Git paths to clusters:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: staging-api
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/infra-config
    targetRevision: main
    path: environments/staging
  destination:
    server: https://staging-cluster.example.com
    namespace: default
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

The selfHeal: true option automatically reverts manual changes to the cluster, ensuring Git remains the source of truth.

Promotion Workflows

Manual Promotion

A developer opens a PR that updates the image tag in the production kustomization. A reviewer approves it, the PR merges, and the GitOps operator deploys:

# Update production image tag
cd environments/production
kustomize edit set image myregistry/api:v1.5.0
git add .
git commit -m "promote api v1.5.0 to production"
git push origin main

Automated Promotion

Use Flux Image Automation to watch a container registry and update image tags in Git automatically:

apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: api
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: api
  policy:
    semver:
      range: ">=1.0.0"
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageUpdateAutomation
metadata:
  name: infra
  namespace: flux-system
spec:
  interval: 5m
  sourceRef:
    kind: GitRepository
    name: infra
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: flux@example.com
        name: Flux
    push:
      branch: main
  update:
    path: ./environments/staging
    strategy: Setters

Flux watches the registry, and when a new semver tag appears, it updates the image tag in the staging kustomization and commits the change. For production, keep promotion manual with a PR-based workflow.

Environment-Specific Overrides

Use Kustomize patches for differences beyond image tags:

# environments/production/patches/api-resources.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    spec:
      containers:
        - name: api
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "1"
              memory: 1Gi
          env:
            - name: LOG_LEVEL
              value: "warn"

Reference the patch in the production kustomization:

# environments/production/kustomization.yaml
patches:
  - path: patches/api-resources.yaml

This cleanly separates what is common (base) from what varies by environment (patches).

Wrap-up

Use a single branch with directory-per-environment layout. Avoid branch-per-environment unless you enjoy merge conflicts. Use Kustomize overlays for environment-specific configuration, automate image updates in staging, and keep production promotions behind a PR review. The repository structure you choose on day one will shape every deployment workflow that follows, so get it right early.