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.
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.yamlKey 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: 8080Dev 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/devMicroservices / 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 overridesTemplate 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:6379Deploy 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-valuesHybrid 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 devIn 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 nameGitOps 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=trueMulti-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
# - mainTroubleshooting: 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 logsIf 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.io2. 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 resourcesPrevention: 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 statusIf "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 readyBest 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 timeMedium/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.hclUse 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
