A production-style GitOps platform running on Kubernetes, demonstrating end-to-end automated delivery using ArgoCD, Helm, and GitHub Actions.
| Skill | Implementation |
|---|---|
| GitOps | ArgoCD self-managed via App-of-Apps pattern; git is the single source of truth |
| Helm | Wrapper charts for all platform components; app chart with ingress, probes, resource limits |
| ArgoCD | Self-management, Application CRs, automated sync with prune/selfHeal, RBAC, GitHub OAuth via Dex |
| Secret Management | Bitnami Sealed Secrets — encrypted at rest in git, decrypted only inside the cluster |
| TLS / DNS | cert-manager with Let's Encrypt DNS-01 (Cloudflare); external-dns for automatic record creation |
| Ingress | Traefik on k3d with TLS termination and cert-manager annotations |
| CI/CD | GitHub Actions — build, Trivy scan, push to GHCR; blocked on CRITICAL/HIGH CVEs |
| Local Kubernetes | k3d cluster with full production parity (Traefik on 80/443) |
| Release Management | Semantic versioning with git-cliff for automated CHANGELOG generation |
flowchart LR
dev([Developer]) -->|git push| gh[GitHub]
subgraph CI ["GitHub Actions (CI)"]
build[Build image] --> scan[Trivy scan\nCRITICAL/HIGH] --> push[Push to GHCR]
end
gh --> CI
subgraph cluster ["Kubernetes (k3d / k3s)"]
direction TB
argocd[ArgoCD] -->|sync| sealed[Sealed Secrets]
argocd -->|sync| cm[cert-manager]
argocd -->|sync| edns[external-dns]
argocd -->|sync| app[demo-app\nHelm chart]
sealed -->|decrypt| secrets[(Secrets)]
cm -->|DNS-01 via Cloudflare| tls[(TLS certs)]
edns -->|A record| cf[Cloudflare DNS]
traefik[Traefik Ingress] -->|routes| app
traefik --- tls
end
gh -->|poll every 3m| argocd
push -->|image tag| app
.
├── app/ # Python Flask demo application + Dockerfile
├── helm/
│ └── demo-app/ # Helm chart — Deployment, Service, Ingress
├── argocd/
│ ├── Makefile # All cluster lifecycle and operational commands
│ ├── k3d-config.yaml # Local cluster definition
│ └── charts/
│ ├── argocd/ # ArgoCD wrapper chart + all Application CRs (App-of-Apps)
│ ├── argocd-secrets/ # SealedSecrets for repo/Helm credentials and OAuth
│ ├── sealed-secrets/ # Bitnami sealed-secrets controller wrapper
│ ├── cert-manager/ # cert-manager wrapper + ClusterIssuers (Cloudflare DNS-01)
│ └── external-dns/ # external-dns wrapper (Cloudflare provider)
├── terraform/ # Infrastructure-as-code (in progress)
└── .github/workflows/
├── ci.yaml # Build → Scan → Push
├── release.yml # Semantic version tagging + GitHub Release
└── update-changelog.yml# Auto-generate CHANGELOG.md on every merge
Requires: k3d, helm, kubectl, argocd, kubeseal
# 1. Create the cluster (Traefik on 80/443)
make -C argocd cluster-up
# 2. Install sealed-secrets controller (must precede ArgoCD)
make -C argocd sealed-secrets
# 3. Bootstrap ArgoCD (Applications disabled — avoids chicken-and-egg)
make -C argocd bootstrap
# 4. Add GitHub repo credentials (fine-grained PAT, read-only)
make -C argocd add-repo-secret GITHUB_USER=<user> GITHUB_PAT=<token>
# 5. Enable ArgoCD self-management (App-of-Apps takes over from here)
make -C argocd self-manageArgoCD UI: http://localhost:8080 (run make -C argocd port-forward in a separate terminal)
For full setup including Cloudflare TLS/DNS, see argocd/README.md.
Every push to main that touches app/:
- Build — Docker image tagged with the commit SHA
- Scan — Trivy checks for CRITICAL and HIGH CVEs; build fails if any unfixed vulnerabilities are found
- Push — Image pushed to GHCR as both
:latestand:<sha>
ArgoCD polls the repo every 3 minutes and syncs any drift automatically.
App-of-Apps — A single ArgoCD Application points at argocd/charts/argocd, which renders all other Application CRs. Adding a new service means adding one YAML file; ArgoCD discovers and deploys it automatically.
Sealed Secrets over external secret stores — Secrets are encrypted with the cluster's public key and committed directly to git. No dependency on Vault or AWS SSM. The trade-off: secrets must be re-sealed after a cluster rebuild.
Bootstrap order — Sealed Secrets controller must exist before ArgoCD starts, because ArgoCD immediately tries to apply SealedSecret resources from git. The Makefile encodes this dependency explicitly.
values.local.yaml committed — Local overrides (single-node Redis, no Istio, admin login) are committed so ArgoCD can render them during self-managed sync. Cluster-only secrets (PATs, image pull secrets) are never committed.