GitOps with ArgoCD: Declarative Kubernetes Deployments
Implement GitOps workflows with ArgoCD for automated, auditable Kubernetes deployments. Learn application management, sync strategies, and multi-cluster patterns.
GitOps treats Git as the single source of truth for infrastructure and applications. ArgoCD makes this practical by continuously reconciling your cluster state with your Git repositories. Push to Git, ArgoCD deploys. Drift detected, ArgoCD corrects.
Core GitOps Principles
- Declarative: Everything is defined as code
- Versioned: Git provides history and audit trail
- Automated: Changes are automatically applied
- Self-healing: Drift is detected and corrected
Installing ArgoCD
# Create namespace
kubectl create namespace argocd
# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for pods
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server -n argocd --timeout=300s
# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
# Port forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
Access at https://localhost:8080 with username admin.
Repository Structure
A GitOps-friendly repository structure:
gitops-repo/
├── apps/ # Application definitions
│ ├── myapp/
│ │ ├── base/ # Base Kubernetes manifests
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── kustomization.yaml
│ │ └── overlays/ # Environment-specific patches
│ │ ├── dev/
│ │ ├── staging/
│ │ └── prod/
│ └── another-app/
├── infrastructure/ # Cluster infrastructure
│ ├── cert-manager/
│ ├── ingress-nginx/
│ └── monitoring/
└── argocd/ # ArgoCD configuration
├── projects/
└── applications/
Creating Applications
Using CLI
# Install ArgoCD CLI
brew install argocd # or download from releases
# Login
argocd login localhost:8080 --insecure
# Create application
argocd app create myapp \
--repo https://github.com/myorg/gitops-repo.git \
--path apps/myapp/overlays/prod \
--dest-server https://kubernetes.default.svc \
--dest-namespace myapp \
--sync-policy automated
Using Declarative YAML
# argocd/applications/myapp.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo.git
targetRevision: main
path: apps/myapp/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
prune: true # Delete resources not in Git
selfHeal: true # Fix drift automatically
allowEmpty: false # Don't sync empty folders
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
Sync Strategies
Automatic Sync
ArgoCD automatically syncs when Git changes:
spec:
syncPolicy:
automated:
prune: true # Remove resources deleted from Git
selfHeal: true # Correct manual changes to cluster
Manual Sync
Require manual intervention (good for production):
spec:
syncPolicy: {} # No automated section = manual sync
Trigger sync via CLI:
argocd app sync myapp
Sync Waves and Hooks
Control deployment order:
# Deploy namespace first
apiVersion: v1
kind: Namespace
metadata:
name: myapp
annotations:
argocd.argoproj.io/sync-wave: "-1" # Deploy before wave 0
---
# Deploy ConfigMap before Deployment
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
annotations:
argocd.argoproj.io/sync-wave: "0"
---
# Deploy app after ConfigMap
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
annotations:
argocd.argoproj.io/sync-wave: "1"
Sync Hooks
Run jobs at specific points:
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/hook: PreSync # Run before sync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: migrate
image: myorg/migrate:latest
command: ["./migrate.sh"]
restartPolicy: Never
Hook types: PreSync, Sync, PostSync, SyncFail
Projects for Multi-Tenancy
Projects organize applications and enforce policies:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-platform
namespace: argocd
spec:
description: Platform team applications
# Allowed source repos
sourceRepos:
- https://github.com/myorg/platform-gitops.git
- https://charts.jetstack.io
# Allowed destinations
destinations:
- namespace: '*'
server: https://kubernetes.default.svc
# Allowed cluster resources
clusterResourceWhitelist:
- group: ''
kind: Namespace
- group: 'rbac.authorization.k8s.io'
kind: ClusterRole
- group: 'rbac.authorization.k8s.io'
kind: ClusterRoleBinding
# Deny certain namespaces
namespaceResourceBlacklist:
- group: ''
kind: Secret
namespace: kube-system
# Roles for team access
roles:
- name: developer
description: Platform team developers
policies:
- p, proj:team-platform:developer, applications, get, team-platform/*, allow
- p, proj:team-platform:developer, applications, sync, team-platform/*, allow
groups:
- platform-team
Kustomize Integration
ArgoCD natively supports Kustomize:
# apps/myapp/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
commonLabels:
app: myapp
# apps/myapp/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: myapp-prod
replicas:
- name: myapp
count: 5
images:
- name: myorg/myapp
newTag: v1.2.3
patches:
- path: deployment-patch.yaml
# apps/myapp/overlays/prod/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
Helm Integration
Deploy Helm charts with ArgoCD:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ingress-nginx
namespace: argocd
spec:
project: default
source:
repoURL: https://kubernetes.github.io/ingress-nginx
chart: ingress-nginx
targetRevision: 4.9.0
helm:
releaseName: ingress-nginx
values: |
controller:
replicaCount: 2
metrics:
enabled: true
resources:
requests:
cpu: 100m
memory: 128Mi
destination:
server: https://kubernetes.default.svc
namespace: ingress-nginx
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
App of Apps Pattern
Manage multiple applications with a single application:
# argocd/applications/root.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo.git
targetRevision: main
path: argocd/applications # Contains all app definitions
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Now every Application YAML in argocd/applications/ is managed by ArgoCD!
ApplicationSet for Dynamic Apps
Generate applications automatically:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: myapp-environments
namespace: argocd
spec:
generators:
# Generate app for each environment
- list:
elements:
- env: dev
cluster: https://kubernetes.default.svc
replicas: 1
- env: staging
cluster: https://kubernetes.default.svc
replicas: 2
- env: prod
cluster: https://prod-cluster.example.com
replicas: 5
template:
metadata:
name: 'myapp-{{env}}'
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo.git
targetRevision: main
path: 'apps/myapp/overlays/{{env}}'
destination:
server: '{{cluster}}'
namespace: 'myapp-{{env}}'
syncPolicy:
automated:
prune: true
selfHeal: true
Secrets Management
ArgoCD works with various secrets solutions:
Sealed Secrets
# Encrypt secrets before committing
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: myapp-secrets
namespace: myapp
spec:
encryptedData:
api-key: AgBy8hqN...encrypted...
External Secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-secrets
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: myapp-secrets
data:
- secretKey: api-key
remoteRef:
key: prod/myapp/api-key
Health Checks
Custom health checks for CRDs:
# argocd-cm ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argocd
data:
resource.customizations.health.cert-manager.io_Certificate: |
hs = {}
if obj.status ~= nil then
if obj.status.conditions ~= nil then
for i, condition in ipairs(obj.status.conditions) do
if condition.type == "Ready" and condition.status == "True" then
hs.status = "Healthy"
hs.message = "Certificate is ready"
return hs
end
end
end
end
hs.status = "Progressing"
hs.message = "Waiting for certificate"
return hs
Notifications
Configure notifications for sync events:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
service.slack: |
token: $slack-token
template.app-sync-succeeded: |
message: |
Application {{.app.metadata.name}} sync succeeded
Revision: {{.app.status.sync.revision}}
trigger.on-sync-succeeded: |
- when: app.status.operationState.phase in ['Succeeded']
send: [app-sync-succeeded]
Key Takeaways
- Git is the source of truth — never kubectl apply manually
- Use automated sync with selfHeal — let ArgoCD fix drift
- Organize with projects — separate teams and environments
- App of Apps — manage applications declaratively
- Sync waves — control deployment order
- External secrets — never commit secrets to Git
- ApplicationSets — generate apps dynamically
“GitOps is not just about automation. It’s about having a complete audit trail, easy rollbacks, and the confidence that your cluster state matches your Git state.”