Reference implementation for running a local Kubernetes cluster with kind, managed via ArgoCD using the App of Apps pattern with sync waves, cert-manager TLS, and Traefik ingress.
This repository demonstrates how to manage a local kind cluster entirely through GitOps using ArgoCD. The structure follows the App of Apps pattern where a single root Application manages all child applications, each of which may manage its own set of child applications and Kubernetes resources.
cluster-my-cluster (root App of Apps, apply this once manually)
├── infra-cert-manager (group parent)
│ ├── infra-cert-manager-helm (Helm chart, wave 1)
│ └── infra-cert-manager-resources (ClusterIssuer, wave 2)
├── infra-traefik (group parent)
│ ├── infra-traefik-helm (Helm chart, wave 1)
│ └── infra-traefik-resources (Middleware, Certificates, IngressRoutes, wave 2)
├── infra-argocd (group parent)
│ └── infra-argocd-resources (ConfigMap, Certificate, IngressRoutes)
├── infra-kubernetes-dashboard (group parent)
│ ├── infra-kubernetes-dashboard-helm
│ └── infra-kubernetes-dashboard-resources
├── app-podinfo (group parent)
│ ├── app-podinfo-helm
│ └── app-podinfo-resources
└── app-whoami (group parent)
└── app-whoami-resources
- App of Apps: each application group has its own
app.yaml(parent) that discovershelm.yamlandresources.yamlwithin its folder - Sync waves: Helm charts deploy at wave 1; dependent resources (ClusterIssuers, IngressRoutes, Certificates) deploy at wave 2
- Retry policy: wave 2 apps retry automatically until dependencies are ready (e.g., cert-manager CRDs exist before ClusterIssuer is applied)
- Cascade finalizer: deleting a parent app cascades to all child apps automatically
- IngressRoute CRDs: Traefik's native CRDs are used for routing instead of standard Ingress resources, with cross-namespace Middleware references
- TLS everywhere: all services use cert-manager-issued TLS certificates from an internal CA
- Docker
- kind
- kubectl
- ArgoCD CLI (optional)
- An internal CA (root or intermediate) with the certificate and private key available locally
kind create cluster --name my-cluster --config configs/kind-config-my-cluster.yamlkubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlBefore ArgoCD syncs cert-manager resources, create the CA secret manually:
kubectl create secret tls internal-ca \
--cert=path/to/ca.crt \
--key=path/to/ca.key \
--namespace cert-managerThe
cert-managernamespace will be created by ArgoCD in a later step, so you may need to create it first:kubectl create namespace cert-manager
Replace all occurrences of <your-github-username> in the YAML files with your actual GitHub username:
find argocd/ -name "*.yaml" -exec sed -i '' 's/<your-github-username>/yourusername/g' {} +Apply the root Application once. ArgoCD manages everything else from this point forward:
kubectl apply -f argocd/kind/my-cluster/app.yaml# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
# Port-forward until the IngressRoute is active
kubectl port-forward svc/argocd-server -n argocd 8080:443Once infra-argocd-resources syncs, ArgoCD will be available at https://argocd.k8s.example.internal (after adding the entry to /etc/hosts).
127.0.0.1 argocd.k8s.example.internal
127.0.0.1 traefik.k8s.example.internal
127.0.0.1 dashboard.k8s.example.internal
127.0.0.1 podinfo.k8s.example.internal
127.0.0.1 whoami.k8s.example.internal
gitops-kind-argocd/
├── argocd/
│ └── kind/
│ └── my-cluster/
│ ├── argocd/
│ │ ├── resources/
│ │ │ ├── argocd-params.yaml
│ │ │ ├── certificate.yaml
│ │ │ └── ingressroute.yaml
│ │ ├── app.yaml
│ │ └── resources.yaml
│ ├── cert-manager/
│ │ ├── resources/
│ │ │ └── clusterissuer.yaml
│ │ ├── app.yaml
│ │ ├── helm.yaml
│ │ └── resources.yaml
│ ├── kubernetes-dashboard/
│ │ ├── resources/
│ │ │ ├── certificate.yaml
│ │ │ ├── ingressroute.yaml
│ │ │ └── serversTransport.yaml
│ │ ├── app.yaml
│ │ ├── helm.yaml
│ │ └── resources.yaml
│ ├── podinfo/
│ │ ├── resources/
│ │ │ ├── certificate.yaml
│ │ │ └── ingressroute.yaml
│ │ ├── app.yaml
│ │ ├── helm.yaml
│ │ └── resources.yaml
│ ├── traefik/
│ │ ├── resources/
│ │ │ ├── certificate-dashboard.yaml
│ │ │ ├── ingressroute-dashboard.yaml
│ │ │ └── middleware-https-redirect.yaml
│ │ ├── app.yaml
│ │ ├── helm.yaml
│ │ └── resources.yaml
│ ├── whoami/
│ │ ├── resources/
│ │ │ ├── certificate.yaml
│ │ │ ├── deployment.yaml
│ │ │ ├── ingressroute.yaml
│ │ │ └── service.yaml
│ │ ├── app.yaml
│ │ └── resources.yaml
│ └── app.yaml
└── configs/
└── kind-config-my-cluster.yaml
- Create a folder under
argocd/kind/my-cluster/<app-name>/ - Add
app.yaml(group parent),helm.yaml(if Helm-based),resources.yaml, and aresources/folder with manifests - The root
cluster-my-clusterapp auto-discovers the newapp.yamlon next sync - Add the hostname to
/etc/hosts
| Placeholder | Replace with |
|---|---|
<your-github-username> |
Your GitHub username |
k8s.example.internal |
Your internal domain |
my-cluster |
Your cluster name |
internal-ca |
Your ClusterIssuer name (if different) |
- The
internal-casecret is created manually and never committed to this repository - No secrets, private keys, or tokens are stored in this repository
- The
resources-finalizer.argocd.argoproj.iofinalizer ensures cascading deletion of child apps server.insecure: "true"inargocd-params.yamldisables ArgoCD's own TLS (Traefik handles it); do not use this in production without a proper ingress setup