Verify the cluster
The deploy reporting success only means OpenTofu finished. Flux then reconciles the platform on its own, and that is where a bootstrap actually succeeds or quietly stalls. Work down the layers — each step tells you which one broke.
The region and cluster name below are the reference values; use whatever you set
in opentofu/config.tm.hcl.
1. Authenticate
The EKS API endpoint is private, so the Tailscale subnet router built in
stage 1 has to be up before kubectl can reach anything:
tailscale status | head -3
aws eks update-kubeconfig --region eu-west-3 --name aws-0
kubectl get nodesNAME STATUS ROLES AGE VERSION
ip-10-0-26-202.eu-west-3.compute.internal Ready <none> 58m v1.36.1-eks-a3a0722
ip-10-0-26-214.eu-west-3.compute.internal Ready <none> 58m v1.36.1-eks-a3a0722Nodes run Bottlerocket. More will appear as Karpenter provisions them — two is the managed node group, not the whole cluster.
Unable to connect to the server: dial tcp ... i/o timeout means the tailnet,
not the cluster. Check tailscale status shows the ip-10-0-* subnet router as
active, and that a stale endpoint is not cached in ~/.kube/config from a
previous build.2. GitOps layer
Flux is the thing that turns an empty cluster into the platform:
flux get kustomizations29 Kustomizations, and on a healthy cluster every one reads True except the
LLM umbrella:
llm-platform True (suspended) waiting to be reconciledThat one is suspended on purpose — see the LLM platform gate. A suspended Kustomization is the expected steady state here, not a failure.
Read the failure, not the cascade. Kustomizations form a dependency graph, so
one broken component reports as a dozen dependency 'flux-system/X' is not ready
messages that name innocent parties. Find the one that is not a dependency
message and start there — then look at events in its namespace, because the true
error is often one level below Flux:
flux get kustomizations | grep -v True
kubectl get events -n <namespace> --sort-by=.lastTimestamp | tail -20A from-scratch bootstrap on 2026-08-29 stalled at 16 of 29 with every message
pointing at security-openbao. The actual error was an ExternalSecret asking for
a key that did not exist, visible only in security namespace events.
3. Secrets
External Secrets pulls from AWS Secrets Manager; cert-manager authenticates to OpenBao with an AppRole delivered the same way:
kubectl get externalsecret -A
kubectl get clusterissuerEvery ExternalSecret should read SecretSynced, and three ClusterIssuers should
be True:
letsencrypt-prod True
letsencrypt-staging True
openbao TrueSecretSyncedError with “Secret does not exist” usually means the key is
wrong rather than the secret missing — compare it against what OpenTofu wrote
(cert_manager_approle_secret_name in opentofu/config.tm.hcl).
4. Certificates and networking
Cilium is the GatewayClass implementation, so Gateway API health is Cilium health:
kubectl get gatewayclass
kubectl get gateway -A
kubectl get certificate -ATwo GatewayClasses, both ACCEPTED=True, and every Gateway PROGRAMMED=True:
NAME CONTROLLER ACCEPTED
cilium io.cilium/gateway-controller True
cilium-tailscale io.cilium/gateway-controller TrueNAMESPACE NAME CLASS PROGRAMMED
infrastructure platform-public cilium True
infrastructure platform-tailscale-admin cilium-tailscale True
infrastructure platform-tailscale-general cilium-tailscale True
security zitadel cilium TrueA GatewayClass stuck at ACCEPTED=Unknown with “Waiting for controller” is the
cilium-operator startup probe, not a broken Gateway. The operator checks for the
Gateway API CRDs once, at startup, and silently disables its Gateway
controller if any are missing. Confirm and fix:
kubectl logs -n kube-system -l io.cilium/app=operator | grep "Required GatewayAPI resources"
kubectl rollout restart -n kube-system deployment/cilium-operator5. Databases
Three CloudNativePG clusters, all reporting healthy — and, importantly, all archiving:
kubectl get sqlinstance -A
kubectl get cluster.postgresql.cnpg.io -A \
-o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,ARCHIVING:.status.conditions[?(@.type=="ContinuousArchiving")].status'NS NAME ARCHIVING
apps xplane-image-gallery-cnpg-cluster True
security xplane-zitadel-cnpg-cluster True
tooling xplane-harbor-cnpg-cluster TrueContinuousArchiving, not just Ready. A cluster whose destination WAL
archive was not cleared before the rebuild starts, reports Ready, and turns
Flux green — with no backups at all. See
Restore a database.6. Browse it
Everything private resolves under *.priv.aws.ogenki.io over the tailnet, with
certificates issued by the OpenBao PKI:
| Service | URL |
|---|---|
| Grafana | https://grafana.priv.aws.ogenki.io |
| Harbor | https://harbor.priv.aws.ogenki.io |
| Headlamp | https://headlamp.priv.aws.ogenki.io |
| Flux UI | https://flux-ui-aws-0.priv.aws.ogenki.io |
| VictoriaMetrics | https://vm.priv.aws.ogenki.io |
| VictoriaLogs | https://vl.priv.aws.ogenki.io |
| VictoriaTraces | https://vt.priv.aws.ogenki.io |
| Hubble UI | https://hubble-ui-aws-0.priv.aws.ogenki.io |
| App Wizard | https://app-wizard.priv.aws.ogenki.io |
| Image gallery | https://image-gallery.priv.aws.ogenki.io |
Hubble UI is on the admin Gateway (tag:admin, group:admin only);
everything else is on the general one. A 403 on Hubble while Grafana works is a
tailnet ACL result, not a broken route.
Single sign-on goes through ZITADEL — see Authentication.
7. Observability is actually receiving data
A Grafana that loads proves nothing; check that data arrives:
kubectl get vmagent,vmalertmanager -n observability
kubectl logs -n observability -l app.kubernetes.io/name=vector --tail=5Then query VictoriaLogs directly:
echo '{kubernetes.pod_namespace="flux-system"} | limit 5' \
| vlogscli -datasource.url='https://vl.priv.aws.ogenki.io/select/logsql/query'Event-driven components (Karpenter, cert-manager, Flux) need a 6–12h window before they look alive — an empty 1h panel is usually the range, not the wiring.
When you are done
Teardown — and read it before you start, not after. The cluster bills from the first apply.
Related
- Access — reaching the VPN, OpenBao, the cluster and the dashboards
- Verify gcp-0 — the same walkthrough for the GCP lane