ArgoCD-managed workloads for the local minikube lab, using the app-of-apps
pattern. One root Application watches apps/ and creates the child Applications.
gitops/
├── bootstrap/
│ ├── root-app.yaml # app-of-apps root (apply this once)
│ └── bootstrap.sh # reproduces imperative state (TLS, LDAP seed, grants)
├── apps/ # child ArgoCD Applications (watched by root)
│ ├── openldap.yaml # Helm chart (external repo) + values from this repo
│ ├── phpldapadmin.yaml # -> manifests/phpldapadmin
│ ├── keycloak.yaml # -> manifests/keycloak (ingress only)
│ ├── keycloak-operator.yaml # -> manifests/keycloak-operator
│ ├── keycloak-cr.yaml # -> manifests/keycloak-cr (incl. keycloak-db SealedSecret)
│ ├── metrics-server.yaml # -> manifests/metrics-server
│ ├── sealed-secrets.yaml # -> manifests/sealed-secrets (controller)
│ ├── cert-manager.yaml # -> manifests/cert-manager (controller)
│ ├── cert-manager-issuers.yaml # -> manifests/cert-manager-issuers (CA + leaf certs)
│ └── react-app.yaml # -> manifests/react-app
├── manifests/
│ ├── openldap/ # values.yaml for the Helm chart (no raw objects)
│ ├── phpldapadmin/ # Deployment + Service + Ingress
│ ├── keycloak/ # just the Ingress (workload is operator-managed)
│ ├── keycloak-operator/ # Keycloak Operator: CRDs + operator deployment
│ ├── keycloak-cr/ # Postgres + Keycloak CR (kc) + KeycloakRealmImport
│ └── react-app/ # nginx + Kustomize ConfigMap (index.html) + Ingress
└── docs/ # step-by-step guides for the whole lab
manifests/vsapps/: files inmanifests/are the payload — for phpLDAPadmin/Keycloak the actualDeployment/Service/Ingressobjects, and for OpenLDAP the Helmvalues.yaml. Files inapps/are ArgoCDApplicationobjects that point at those and keep them synced. OpenLDAP installs from an external Helm chart rather than raw YAML, so itsmanifests/openldap/folder holds only the chart values, not Kubernetes objects.
Full walkthroughs in docs/:
- openldap-minikube-guide.md — install OpenLDAP + phpLDAPadmin
- keycloak-minikube-guide.md — install Keycloak
- keycloak-openldap-federation-guide.md — federate Keycloak to OpenLDAP (+
keycloak-ldap-federation.sh) - argocd-gitops-guide.md — this GitOps setup, adoption, and the
imagePullSecretsgotcha - rebuild-runbook.md — recreate the whole lab from scratch (Git +
bootstrap.sh)
- Managed by ArgoCD: OpenLDAP, phpLDAPadmin, the Keycloak Operator +
KeycloakCR +KeycloakRealmImport(so thedemorealm structure is now declarative too), and react-app. - NOT managed by ArgoCD: the Keycloak
demorealm and the LDAP user federation — those are configured via the Keycloak Admin API / thekeycloak-ldap-federation.shscript, because they aren't plain Kubernetes objects.
-
Push this repo to your Git remote (
https://github.com/shaqa3/homelab-gitops.git). The child Application manifests already point at it; theopenldapapp uses the upstream Helm repo and needs no change. -
Apply the root app once:
kubectl apply -f bootstrap/root-app.yaml
-
Watch ArgoCD reconcile:
kubectl -n argocd get applications.argoproj.io -w
OpenLDAP installs from the upstream Helm chart (helm-openldap/openldap 2.0.4),
but its values live in this repo at
manifests/openldap/values.yaml. The
apps/openldap.yaml Application wires them in with a multi-source setup: the
chart from the Helm repo, the values file from this Git repo, joined via a
$values reference (valueFiles: [$values/manifests/openldap/values.yaml]).
| Value | Meaning |
|---|---|
replicaCount: 1 |
Single instance — no HA for a local lab. |
image.repository/tag |
osixia/openldap:1.5.0, the public OpenLDAP image. |
image.pullSecret: "no-pull-secret" |
Must be a non-empty name. The chart always renders an imagePullSecrets entry from this; "" produces a null-named entry that breaks ArgoCD's diff and makes self-heal append empties forever. The named secret need not exist — the public image pulls anonymously. |
replication.enabled: false |
No multi-master replication. |
tls.enabled: false / LDAP_TLS: "false" |
Plain LDAP on port 389 (simpler for local testing). |
LDAP_ORGANISATION / LDAP_DOMAIN |
Example Inc. / example.org → base DN dc=example,dc=org. |
LDAP_BACKEND: "mdb" |
LMDB storage backend (the OpenLDAP default). |
adminPassword / configPassword |
Bind passwords for cn=admin,dc=example,dc=org and cn=admin,cn=config. |
persistence.enabled: true, size: 1Gi |
A 1Gi PVC so LDAP data survives pod restarts. |
ltb-passwd.enabled: false, phpldapadmin.enabled: false |
Disable the chart's bundled UIs (they ship a deprecated Ingress apiVersion). phpLDAPadmin is deployed separately under manifests/phpldapadmin. |
service.* |
ClusterIP exposing 389 (LDAP) and 636 (LDAPS). |
To change OpenLDAP config, edit manifests/openldap/values.yaml, commit, and
push — ArgoCD re-renders the chart and syncs.
The React login demo (documented in
manifests/react-app/README.md) is served at
http://react.192.168.64.3.nip.io by a stock nginx:alpine with the app's
index.html provided by a Kustomize configMapGenerator — so there's no
custom image to build or push, and the whole thing is reproducible from Git.
Kustomize hashes the ConfigMap name from the file contents, so editing
manifests/react-app/index.html and pushing automatically rolls the pod.
The app authenticates against Keycloak's demo realm using the public client
react-app. That client's Valid redirect URIs / Web origins include
http://react.192.168.64.3.nip.io (alongside the local dev ports). The Keycloak
client itself is not in Git (it's realm config, like the LDAP federation).
Keycloak runs via the Keycloak Operator rather than a hand-rolled Deployment:
manifests/keycloak-operator/— the operator's CRDs (Keycloak,KeycloakRealmImport) and operator Deployment (appkeycloak-operator, sync-wave-1so CRDs exist first).manifests/keycloak-cr/— Postgres + aKeycloakCR namedkc(prod mode,httpenabled with TLS terminated at the ingress,proxy: xforwarded) + aKeycloakRealmImportthat declaratively recreates thedemorealm (react-app client, realm roles, LDAP federation + mappers).manifests/keycloak/— now just the Ingress, pointed at the operator'skc-service.
The realm import JSON was built from a realm export with the masked LDAP
bindCredential re-injected. Two things are not in Git (they're identity /
per-user state, not realm structure): the admin/admin master user (recreated
once; persists in Postgres) and the realm-management role grants to
jdoe/asmith (federated users aren't part of the realm JSON). LDAP users
themselves re-import from OpenLDAP automatically.
Editing
manifests/keycloak-cr/realmimport.yamland pushing re-runs the import job. The operator generates an initial admin in thekc-initial-adminsecret; the lab's permanentadmin/adminwas created on top of that.
TLS is issued by cert-manager — a self-signed root → a homelab-ca
ClusterIssuer → one leaf Certificate per namespace, each producing the
kubernetes.io/tls secret homelab-tls the Ingresses reference
(manifests/cert-manager + manifests/cert-manager-issuers). It's declarative
and auto-renewing — no out-of-band mkcert step. The only manual bit is trusting
the CA once (same as any self-signed CA):
kubectl -n cert-manager get secret homelab-ca-key-pair \
-o jsonpath='{.data.tls\.crt}' | base64 -d > homelab-ca.crt
# macOS: sudo security add-trusted-cert -d -r trustRoot \
# -k /Library/Keychains/System.keychain homelab-ca.crt- react-app forces HTTPS (PKCE needs a secure context). keycloak sets
ssl-redirect: "false"so HTTP stays usable for CLI tooling; the browser app useshttps://keycloakexplicitly.
Secrets are encrypted in Git with Sealed Secrets: the controller
(manifests/sealed-secrets) decrypts keycloak-db
(manifests/keycloak-cr/db-sealedsecret.yaml) into a real Secret in-cluster. Seal
new secrets with kubeseal --controller-namespace kube-system. Caveat: the
controller's private key decrypts everything — back it up, or the committed
SealedSecrets are unrecoverable after a cluster rebuild (see the rebuild runbook).
The LDAP bindCredential stays inline in the realm-import CR (no secretRef to
seal); it's the lab LDAP admin password.
automated.prune+selfHealare on: ArgoCD reverts out-of-bandkubectlchanges and deletes resources removed from Git.- The
*.nip.ioingress hostnames embed the minikube IP (192.168.64.3). If the IP changes, update the Ingress hosts and commit. - Secrets (Keycloak bootstrap admin, LDAP passwords) are in plaintext here for a local lab. For anything real, use Sealed Secrets / External Secrets instead.