Cloud Native 20 min read

Kubernetes Multi-Environment Management: Config Sync, GitOps & Troubleshooting

This guide covers Kubernetes multi-environment management using Kustomize and Helm for configuration layering, ArgoCD for GitOps automation, CI/CD pipelines with GitLab, troubleshooting common sync/permission/drift issues, and team-size-specific best practices for dev/test/prod environments.

Cilantro Loves Linux
Cilantro Loves Linux
Cilantro Loves Linux
Kubernetes Multi-Environment Management: Config Sync, GitOps & Troubleshooting

Why Multi-Environment Management Is Painful

Kubernetes operators often face configuration drift: a setup that works in development fails in test, and production hotfixes reveal that dev/test configs have diverged. Managing dev/test/prod is like tailoring one garment for three sizes — you need consistent base configuration while accommodating environment-specific differences. Manual YAML edits are error-prone and inefficient. The 2025 cloud-native consensus: use layered configuration tools, GitOps for automatic synchronization, and a troubleshooting playbook.

Tool Selection: Kustomize vs Helm

Core Differences

Choosing between Kustomize and Helm is not binary; mainstream practice combines them based on need.

Kustomize : Declarative patches (no templating), low learning curve (native K8s syntax), weak dependency management, suits small-to-medium apps and fine-grained config tweaks.

Helm : Template-based configuration (variable placeholders), moderate learning curve (Go templates, Chart structure), strong dependency management via Umbrella Charts, suits microservice clusters, large-scale deployments, and many third-party components.

Implementation Patterns

Small Teams / Simple Apps: Kustomize + ArgoCD

Ideal for 3–5 person teams with few components (e.g., one backend + one database). Core pattern: Base + Overlay layering.

Directory Structure

app-config/
├── base/                 # Shared base configuration
│   ├── deployment.yaml   # Common deployment (image, ports, mounts)
│   ├── service.yaml      # Common service (port mapping)
│   └── kustomization.yaml
└── overlay/              # Environment-specific modifications
    ├── dev/              # Dev: fewer replicas, debug enabled
    │   ├── replica-patch.yaml
    │   └── kustomization.yaml
    ├── test/             # Test: resource limits, verbose logging
    │   ├── resource-patch.json
    │   └── kustomization.yaml
    └── prod/             # Prod: multiple replicas, HPA
        ├── hpa.yaml
        └── kustomization.yaml

Key Code Examples

Base deployment (unchanged across environments):

# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend
        image: my-app:latest  # Image tag updated via CI
        ports:
        - containerPort: 8080

Dev overlay patch (replica count + debug env):

# overlay/dev/replica-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: backend
        env:
        - name: DEBUG
          value: "true"

Deploy and inspect commands:

# Deploy dev environment
kubectl apply -k overlay/dev
# View final rendered config (critical for debugging)
kubectl kustomize overlay/dev

Microservices / Large-Scale: Helm + ArgoCD

For 5+ microservices (gateway, user service, order service, etc.), Helm's templating and dependency management eliminate 80% of duplicate config. Example: user-service Chart.

Chart Structure (Values Layering)

user-service/
├── charts/               # Third-party dependencies (e.g., Redis)
├── templates/            # Config templates (write once, reuse)
│   ├── deployment.yaml
│   └── service.yaml
├── values.yaml           # Base defaults
├── values-dev.yaml       # Dev overrides
├── values-test.yaml      # Test overrides
└── values-prod.yaml      # Prod overrides

Template with Placeholders

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-user-service
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
      - name: user-service
        image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
        resources:
{{ toYaml .Values.resources | nindent 12 }}
        env:
        - name: REDIS_ADDR
          value: {{ .Values.redis.addr }}

Prod Values Override (Only Differences)

# values-prod.yaml
replicaCount: 5
image:
  tag: v1.2.0  # Fixed version, not latest
resources:
  limits:
    cpu: 2
    memory: 4Gi
  requests:
    cpu: 1
    memory: 2Gi
redis:
  addr: redis-prod:6379

Deploy Commands

# Install prod
helm install user-prod ./user-service --values values-prod.yaml -n prod
# Upgrade preserving manual changes
helm upgrade user-prod ./user-service --values values-prod.yaml --reuse-values

Hybrid Approach: Kustomize for Custom Services, Helm for Third-Party

2025 mainstream pattern: Kustomize manages home-grown services (fine-grained control), Helm deploys Redis/MySQL/etc. (reuse community Charts).

Install Redis via Helm (Bitnami chart):

helm repo add bitnami https://charts.bitnami.com/bitnami
helm install redis-dev bitnami/redis --values redis-dev.yaml -n dev

In Kustomize base, reference the Helm-deployed Redis address via configMapGenerator:

# base/kustomization.yaml
configMapGenerator:
- name: app-config
  literals:
  - REDIS_ADDR=redis-dev-master:6379  # References Helm service name

GitOps Automation: From Manual Deploy to Commit-and-Sync

Tools solve "how to configure"; GitOps solves "how to sync across environments." ArgoCD is the 2025 standard: Git repo as source of truth, cluster state follows Git — no more manual kubectl apply.

Core Workflow

Developer commits code → GitLab CI triggers → Build image & push → Update image tag in Git config → ArgoCD detects Git change → Auto-sync to dev → After test passes, sync to test → After approval, sync to prod → ArgoCD verifies resource health.

ArgoCD Multi-Environment Configuration

Single Environment Application (Small Teams)

# argocd-app-dev.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-dev
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://git.example.com/your-repo/app-config.git
    targetRevision: HEAD
    path: overlay/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: dev
  syncPolicy:
    automated:
      prune: true        # Delete resources not in Git
      selfHeal: true     # Auto-repair drift (e.g., manual replica change)
    syncOptions:
    - CreateNamespace=true

Multi-Environment ApplicationSet (Medium/Large Teams)

Manages dev/test/prod with a single definition, generating sync rules per environment label.

# argocd-appset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: app-all-envs
  namespace: argocd
spec:
  generators:
  - list:
      elements:
      - env: dev
        path: overlay/dev
        namespace: dev
      - env: test
        path: overlay/test
        namespace: test
      - env: prod
        path: overlay/prod
        namespace: prod
  template:
    metadata:
      name: app-{{.env}}
    spec:
      project: default
      source:
        repoURL: https://git.example.com/your-repo/app-config.git
        targetRevision: HEAD
        path: {{.path}}
      destination:
        server: https://kubernetes.default.svc
        namespace: {{.namespace}}
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        {{- if eq .env "prod" }}
        syncOptions:
        - CreateNamespace=true
        manualSync: {}  # Require manual approval for prod
        {{- else }}
        syncOptions:
        - CreateNamespace=true
        {{- end }}

CI/CD Pipeline: GitLab CI + ArgoCD Closed Loop

ArgoCD alone isn't enough; GitLab CI implements "code commit → auto deploy". Key stages in .gitlab-ci.yml:

stages:
  - build          # Build image
  - update-config  # Update Git config (image tag)
  - sync-argocd    # Trigger ArgoCD sync (optional)

# 1. Build image (Docker Buildx, multi-platform)
build-image:
  stage: build
  image: docker:24.0.5
  services:
    - docker:24.0.5-dind
  script:
    - docker login -u $REGISTRY_USER -p $REGISTRY_PWD $REGISTRY_URL
    - IMAGE_TAG=$CI_COMMIT_SHORT_SHA  # Unique tag from Git commit hash
    - docker build -t $REGISTRY_URL/my-app:$IMAGE_TAG .
    - docker push $REGISTRY_URL/my-app:$IMAGE_TAG
  only:
    - develop   # dev branch
    - test      # test branch
    - main      # prod branch

# 2. Update Git config with new image tag (sed on Kustomize file)
update-config:
  stage: update-config
  image: alpine:3.18
  script:
    - apk add git
    - git config --global user.name "CI Bot"
    - git config --global user.email "[email protected]"
    - git clone $CI_REPOSITORY_URL config-repo
    - cd config-repo
    - if [ "$CI_COMMIT_BRANCH" = "develop" ]; then ENV_DIR="overlay/dev"; fi
    - if [ "$CI_COMMIT_BRANCH" = "test" ]; then ENV_DIR="overlay/test"; fi
    - if [ "$CI_COMMIT_BRANCH" = "main" ]; then ENV_DIR="overlay/prod"; fi
    - sed -i "s|image: $REGISTRY_URL/my-app:.*|image: $REGISTRY_URL/my-app:$CI_COMMIT_SHORT_SHA|g" $ENV_DIR/deployment.yaml
    - git add $ENV_DIR/deployment.yaml
    - git commit -m "Update image tag to $CI_COMMIT_SHORT_SHA for $CI_COMMIT_BRANCH"
    - git push
  only:
    - develop
    - test
    - main

# 3. Trigger ArgoCD sync (optional; ArgoCD also auto-detects)
sync-argocd:
  stage: sync-argocd
  image: curlimages/curl:8.4.0
  script:
    - ARGOCD_APP_NAME="app-$(echo $CI_COMMIT_BRANCH | sed 's/develop/dev/;s/main/prod/')"
    - curl -X POST -k -u $ARGOCD_USER:$ARGOCD_PWD https://argocd.example.com/api/v1/applications/$ARGOCD_APP_NAME/sync
  only:
    - develop
    - test
    # prod commented out for manual approval
    # - main

Troubleshooting: 3 High-Frequency Issues & Resolution Steps

1. ArgoCD Sync Fails: "Insufficient Permissions"

Steps:

Check ArgoCD sync logs:

argocd app logs app-dev --sync  # View dev sync logs

If logs show "no permissions to create deployment", inspect ArgoCD service account role bindings in the target namespace: kubectl get rolebinding -n dev | grep argocd Fix: Grant ArgoCD service account edit role in the namespace:

# argocd-dev-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: argocd-edit
  namespace: dev
subjects:
- kind: ServiceAccount
  name: argocd-application-controller
  namespace: argocd
roleRef:
  kind: ClusterRole
  name: edit
  apiGroup: rbac.authorization.k8s.io

2. Config Drift: Cluster Replica Count Differs from Git

Steps:

Compare Git config with live cluster state: kubectl diff -k overlay/prod If output shows spec.replicas: 5 → 3, someone manually changed replicas. With selfHeal: true ArgoCD auto-repairs; otherwise force sync:

argocd app sync app-prod --prune  # Force sync and delete extra resources

Prevention: Ensure selfHeal: true in syncPolicy and restrict manual cluster modifications via RBAC.

3. Helm Deploy Fails: "Dependency Not Ready"

Steps:

Check Helm release status:

helm status user-prod -n prod  # View prod release status

If "waiting for redis service", verify Redis pod readiness: kubectl get pod -n prod | grep redis Fix: Add dependency condition in values-prod.yaml so user-service waits for Redis:

# values-prod.yaml
dependencies:
- name: redis
  condition: redis.enabled  # Deploy only when Redis is enabled and ready

Best Practices by Team Size

Small Teams (3–5 People): Simplicity First

Toolchain: Kustomize + ArgoCD (single cluster)

Simplifications:

No ApplicationSet; create three independent Applications (dev/test/prod).

Sensitive data via Kustomize secretGenerator (no Vault needed):

# base/kustomization.yaml
secretGenerator:
- name: db-cred
  literals:
  - username=dbuser
  files:
  - password.txt  # Local file, not committed; injected at CI time

Medium/Large Teams (10+ People): Security + Efficiency

Toolchain: Helm + ArgoCD + Vault + KubeSphere

Key Configurations:

Store secrets in Vault, inject via sidecar:

# templates/deployment.yaml with Vault sidecar
spec:
  containers:
  - name: backend
    # ... other config
  initContainers:
  - name: vault-agent-init
    image: hashicorp/vault:1.15.0
    command: ["vault", "agent", "-config=/vault/config.hcl"]
    volumeMounts:
    - name: vault-config
      mountPath: /vault/config.hcl

Use KubeSphere for multi-cluster management, unified view of dev/test/prod resource status.

Conclusion

Kubernetes multi-environment management need not be over-engineered; the core is "match team size, land something usable." Small teams start with Kustomize + ArgoCD to achieve "commit → sync"; larger teams add Helm's dependency management and Vault for secret protection. Remember one principle: never do manually what tools can automate — Git manages config, ArgoCD manages sync, CI manages build. Reduce human intervention, and failures naturally decrease. Following the code in this guide, most teams can run the full pipeline within 1–2 weeks.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CI/CDKubernetestroubleshootingGitOpsHelmArgoCDKustomizeMulti-environment
Cilantro Loves Linux
Written by

Cilantro Loves Linux

Focused on Linux system operations and maintenance technology exchange, regularly publishing in-depth technical articles covering core areas such as system architecture, performance optimization, and security hardening. Whether you are a beginner or a seasoned expert, you can find valuable resources to enhance your skills and solve problems here. Follow us to explore the mysteries of Linux operations!

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.