What GitOps Actually Means
GitOps is an operational framework where Git is the single source of truth for your infrastructure and application configuration. Instead of running kubectl apply manually or triggering imperative scripts, you declare your desired state in Git and let an operator reconcile it automatically.
The core principles are straightforward:
- Declarative configuration. Everything is described as code in Git.
- Version controlled. Every change goes through a pull request with review.
- Automated reconciliation. An agent continuously ensures the live state matches the desired state.
- Self-healing. Drift from the desired state is detected and corrected automatically.
ArgoCD is the most widely adopted GitOps operator for Kubernetes. In this guide, we walk through a complete implementation from scratch.
Installing ArgoCD
Deploy ArgoCD into your cluster using the official manifests or Helm:
# Option 1: Official manifests
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Option 2: Helm (recommended for production)
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--values argocd-values.yaml
Production Helm values:
# argocd-values.yaml
server:
replicas: 2
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 5
ingress:
enabled: true
ingressClassName: nginx
hosts:
- argocd.example.com
tls:
- secretName: argocd-tls
hosts:
- argocd.example.com
controller:
replicas: 2
resources:
requests:
memory: 512Mi
cpu: 250m
limits:
memory: 1Gi
repoServer:
replicas: 2
resources:
requests:
memory: 256Mi
cpu: 100m
redis-ha:
enabled: true
configs:
params:
server.insecure: false
application.instanceLabelKey: argocd.argoproj.io/instance
Retrieve the initial admin password:
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
Repository Structure for GitOps
We recommend separating application source code from deployment manifests. This is the pattern that scales:
# Application repo (app developers own this)
my-api/
├── src/
├── Dockerfile
├── .github/workflows/build.yml
└── ...
# Deployment repo (platform team + app developers)
k8s-deployments/
├── apps/
│ ├── my-api/
│ │ ├── base/
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ ├── hpa.yaml
│ │ │ └── kustomization.yaml
│ │ └── overlays/
│ │ ├── dev/
│ │ │ ├── kustomization.yaml
│ │ │ └── patch-replicas.yaml
│ │ ├── staging/
│ │ └── production/
│ └── another-service/
├── infrastructure/
│ ├── cert-manager/
│ ├── ingress-nginx/
│ └── monitoring/
└── projects/
└── platform.yaml
Kustomize Base and Overlays
The base contains your default manifests:
# apps/my-api/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
spec:
replicas: 2
selector:
matchLabels:
app: my-api
template:
metadata:
labels:
app: my-api
spec:
containers:
- name: my-api
image: myregistry.com/my-api:latest
ports:
- containerPort: 8080
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10
readinessProbe:
httpGet:
path: /readyz
port: 8080
Environment overlays customize specific values:
# apps/my-api/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- ../../base
patches:
- path: patch-replicas.yaml
images:
- name: myregistry.com/my-api
newTag: v1.4.2
# apps/my-api/overlays/production/patch-replicas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
spec:
replicas: 5
Creating ArgoCD Applications
Define your applications as Kubernetes resources (the "App of Apps" pattern):
# projects/platform.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: platform
namespace: argocd
spec:
description: Platform services
sourceRepos:
- "https://github.com/mycompany/k8s-deployments.git"
destinations:
- namespace: "*"
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: "*"
kind: "*"
# apps/my-api/argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-api-production
namespace: argocd
labels:
app.kubernetes.io/part-of: platform
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: platform
source:
repoURL: https://github.com/mycompany/k8s-deployments.git
targetRevision: main
path: apps/my-api/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
Key settings explained:
selfHeal: true-- ArgoCD reverts manual changes made withkubectl. This enforces Git as the single source of truth.prune: true-- resources deleted from Git are deleted from the cluster.PruneLast: true-- deletion happens after all other sync operations complete.
The CI/CD Pipeline Integration
Your CI pipeline builds and pushes images. It then updates the deployment repo to trigger ArgoCD:
# .github/workflows/build.yml (in the application repo)
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Build and push image
uses: docker/build-push-action@v5
with:
push: true
tags: myregistry.com/my-api:${{ github.sha }}
- name: Update deployment manifest
run: |
git clone https://x-access-token:${{ secrets.DEPLOY_TOKEN }}@github.com/mycompany/k8s-deployments.git
cd k8s-deployments
# Update the image tag in kustomization
cd apps/my-api/overlays/production
kustomize edit set image myregistry.com/my-api:${{ github.sha }}
git config user.name "github-actions"
git config user.email "[email protected]"
git add .
git commit -m "deploy: my-api ${{ github.sha }}"
git push
ArgoCD detects the commit, compares the rendered manifests against the live state, and performs a sync automatically.
Progressive Delivery with Argo Rollouts
For production environments, replace standard Deployments with Argo Rollouts for canary or blue-green deployments:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-api
namespace: production
spec:
replicas: 5
selector:
matchLabels:
app: my-api
template:
metadata:
labels:
app: my-api
spec:
containers:
- name: my-api
image: myregistry.com/my-api:v1.4.2
ports:
- containerPort: 8080
strategy:
canary:
steps:
- setWeight: 10
- pause: { duration: 5m }
- setWeight: 30
- pause: { duration: 5m }
- setWeight: 60
- pause: { duration: 5m }
analysis:
templates:
- templateName: success-rate
startingStep: 1
canaryService: my-api-canary
stableService: my-api-stable
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
metrics:
- name: success-rate
interval: 60s
successCondition: result[0] > 0.95
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{service="my-api",status!~"5.."}[5m]))
/
sum(rate(http_requests_total{service="my-api"}[5m]))
This configuration progressively shifts traffic from 10% to 30% to 60% to 100%, with automated rollback if the success rate drops below 95%.
Multi-Cluster Management
ArgoCD can manage multiple clusters from a single control plane:
# Add a remote cluster
argocd cluster add my-staging-cluster --name staging
# Create an application targeting the remote cluster
argocd app create my-api-staging \
--repo https://github.com/mycompany/k8s-deployments.git \
--path apps/my-api/overlays/staging \
--dest-server https://staging-api.example.com \
--dest-namespace staging
For managing many applications across clusters, use ApplicationSets:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-api
namespace: argocd
spec:
generators:
- list:
elements:
- cluster: production
url: https://kubernetes.default.svc
- cluster: staging
url: https://staging-api.example.com
template:
metadata:
name: "my-api-{{cluster}}"
spec:
project: platform
source:
repoURL: https://github.com/mycompany/k8s-deployments.git
targetRevision: main
path: "apps/my-api/overlays/{{cluster}}"
destination:
server: "{{url}}"
namespace: "{{cluster}}"
Operational Best Practices
- Use Projects to enforce RBAC. Restrict which repos and namespaces each team can deploy to.
- Enable notifications. ArgoCD has a built-in notification controller that can send Slack messages, GitHub status updates, and webhook calls on sync events.
- Monitor ArgoCD itself. It exposes Prometheus metrics at
/metrics. Alert on sync failures and high reconciliation times. - Set resource limits on ArgoCD components. The repo-server in particular can consume significant memory when rendering large manifests.
- Use sync windows to prevent deployments during maintenance windows or outside business hours.
Conclusion
GitOps with ArgoCD transforms Kubernetes deployment from an error-prone manual process into an automated, auditable, and self-healing system. The combination of declarative configuration, automated reconciliation, and progressive delivery gives teams the confidence to deploy frequently without the risk.
At DevOpsVibe, we implement GitOps pipelines that take teams from manual deployments to fully automated, multi-cluster delivery. Whether you are adopting GitOps for the first time or scaling an existing setup, we can help you get there faster. Let us know how we can help.