GitOps with ArgoCD on EKS: A Practical Tutorial
DevOps / SRE Engineer & CEO, Cloud DevOps Solutions
GitOps means using Git as the single source of truth for your Kubernetes deployments. Merge a change to a YAML file in Git, and ArgoCD automatically applies it to the cluster, no kubectl apply from a pipeline, no manual hotfixes. Here is how to set it up end-to-end on EKS.
GitOps principles (quick recap)
- Declarative: describe desired state, not imperative commands
- Versioned: all changes in Git, reviewable and revertable
- Automated sync: system continuously reconciles actual with desired state
- Observed: deviations (drift) are detected and reported
ArgoCD implements all four. It runs in-cluster, watches Git, and applies changes. The pipeline’s only job is to push a new image tag to Git; ArgoCD handles the rest.
Install ArgoCD on EKS
# Create namespace
kubectl create namespace argocd
# Install ArgoCD (stable channel)
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=120s
# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d && echo
# Port-forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Open https://localhost:8080, login: admin / <password above>
For production, expose via ALB:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server
namespace: argocd
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/backend-protocol: HTTPS
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:...
spec:
rules:
- host: argocd.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 443
Install the ArgoCD CLI
brew install argocd
# Login
argocd login argocd.example.com \
--username admin \
--password YOUR_PASSWORD \
--grpc-web
Connect a Git repository
# HTTPS (public repo)
argocd repo add https://github.com/your-org/k8s-manifests
# HTTPS (private repo)
argocd repo add https://github.com/your-org/k8s-manifests \
--username git \
--password YOUR_GITHUB_TOKEN
# SSH (recommended for production)
argocd repo add [email protected]:your-org/k8s-manifests \
--ssh-private-key-path ~/.ssh/argocd_deploy_key
Create your first Application
# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-staging
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/k8s-manifests
targetRevision: main
path: apps/my-app/staging
destination:
server: https://kubernetes.default.svc
namespace: staging
syncPolicy:
automated:
prune: true # delete resources removed from Git
selfHeal: true # revert manual kubectl changes
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
kubectl apply -f application.yaml
# Check sync status
argocd app get my-app-staging
argocd app sync my-app-staging # manual sync if needed
Deploy a Helm chart with ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-production
namespace: argocd
spec:
source:
repoURL: https://github.com/your-org/k8s-manifests
targetRevision: main
path: helm/my-app
helm:
valueFiles:
- values.yaml
- values-production.yaml
parameters:
- name: image.tag
value: "abc1234" # updated by CI/CD pipeline
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
CI/CD → ArgoCD integration
The pipeline builds and pushes the image, then updates the image tag in Git. ArgoCD detects the change and syncs.
# .github/workflows/deploy.yml (relevant section)
- name: Update image tag in manifests
run: |
git clone https://github.com/your-org/k8s-manifests deploy-repo
cd deploy-repo
# Update the image tag in the Helm values file
sed -i "s|tag:.*|tag: \"${GITHUB_SHA::8}\"|" helm/my-app/values-production.yaml
git config user.email "[email protected]"
git config user.name "CI Bot"
git add helm/my-app/values-production.yaml
git commit -m "deploy: update my-app to ${GITHUB_SHA::8}"
git push
ArgoCD detects the push and syncs within 3 minutes (configurable). For faster sync, use webhook notifications.
ApplicationSets for multi-environment
Manage all environments from a single definition:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app
namespace: argocd
spec:
generators:
- list:
elements:
- env: staging
namespace: staging
values_file: values-staging.yaml
- env: production
namespace: production
values_file: values-production.yaml
template:
metadata:
name: 'my-app-{{env}}'
spec:
project: default
source:
repoURL: https://github.com/your-org/k8s-manifests
targetRevision: main
path: helm/my-app
helm:
valueFiles:
- values.yaml
- '{{values_file}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{namespace}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Sealed Secrets for encrypted secrets in Git
Never store plaintext secrets in Git. Sealed Secrets encrypts them with the cluster’s public key.
# Install Sealed Secrets controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets -n kube-system
# Install kubeseal CLI
brew install kubeseal
# Seal a secret
kubectl create secret generic my-db-secret \
--from-literal=password=supersecret \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-db-secret.yaml
# Commit sealed-db-secret.yaml to Git, safe to do
git add sealed-db-secret.yaml && git commit -m "add sealed db secret"
The controller decrypts on the cluster; no one else can decrypt without the private key.
RBAC: least-privilege access
# ArgoCD RBAC policy (in argocd-rbac-cm ConfigMap)
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argocd
data:
policy.default: role:readonly
policy.csv: |
# Developers can sync staging only
p, role:developer, applications, sync, staging/*, allow
p, role:developer, applications, get, */*, allow
# Ops can sync all environments
p, role:ops, applications, *, */*, allow
p, role:ops, clusters, get, *, allow
# Bind roles to groups (from SSO)
g, github-org:developers, role:developer
g, github-org:ops, role:ops
Key takeaways
- GitOps with ArgoCD makes deployments auditable, reversible, and drift-detectable,
kubectl applyfrom pipelines is the anti-pattern. - Enable
selfHeal: trueto automatically revert manual changes; this is the whole point of GitOps. - Use ApplicationSets to manage multiple environments from a single definition rather than duplicating Application manifests.
- Sealed Secrets is the correct way to store Kubernetes secrets in Git, never commit plaintext.
- Your pipeline’s job is to update the image tag in Git; ArgoCD does the rest. Keep these concerns separate.
- Set up webhook notifications from GitHub to ArgoCD for near-instant sync instead of the default 3-minute polling interval.