Setup Troubleshooting Guide¶
Fresh-clone setup issues are expected — this template bundles a lot of moving parts (Kind, nginx, ArgoCD, Prometheus, Backstage, Docker Compose) that must initialise in the right order. This guide covers the issues most commonly encountered on a first install and what to do about them.
The single most important rule: setup.sh must complete successfully before bootstrap-local.sh runs. Many downstream failures (empty catalog, ArgoCD generating no apps, GitHub OAuth loops) trace back to skipping or partially running setup.sh.
Quick diagnosis checklist¶
Run this after any failure to orient yourself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Phase 0 — Personalisation (setup.sh)¶
Symptom: ArgoCD shows no applications¶
Cause: setup.sh was not run (or the xargs path was used on an older checkout), so local/argocd/app-of-apps-local.yaml still contains the moatazeldebsy placeholder. ArgoCD's git generator finds no matching directories and creates no apps.
Fix:
1 2 3 4 5 6 7 8 | |
Symptom: Services in catalog still show moatazeldebsy URLs¶
Cause: setup.sh was run but the find-replace scan missed some files (can happen if you ran an older version that used xargs instead of while-read).
Fix:
1 2 3 4 5 6 7 | |
Phase 1 — Local bootstrap (bootstrap-local.sh)¶
Symptom: kind create cluster fails immediately¶
Causes and fixes:
| Cause | Fix |
|---|---|
| Docker not running | Start Docker Desktop / Rancher Desktop and wait for it to be ready |
| Port 80 already bound | lsof -i :80 — stop the conflicting process (another nginx, a web server, etc.) |
| Stale Kind cluster with same name | kind delete cluster --name idp-mvp && ./scripts/bootstrap-local.sh |
| Rancher Desktop Traefik still enabled | Disable in Preferences → Kubernetes → disable Traefik |
Symptom: nginx ingress pods pending, *.idp.local not reachable¶
Cause: Port 80 or 443 is bound by another process on the host.
scripts/lib.shruns a preflight check for this and aborts a fresh install before creating the cluster if either port is bound, printing Rancher Desktop/Traefik-specific advice. The check is deliberately skipped when the Kind cluster already exists — at that point the bound port is usually the cluster's own ingress controller, and failing would block every day-2 re-run. So if you hit this symptom on an existing cluster, the preflight will not warn you; work through the checks below.
1 2 3 4 5 6 7 8 | |
Symptom: many/all *.idp.local services return 503 or 504 at once¶
Cause: the Kind cluster's host (Docker Desktop) is oversubscribed — the
3-node cluster plus Backstage/Postgres are asking for more CPU/memory than the
host has, so kube-apiserver itself starts timing out under load. That
timeout cascades into seemingly unrelated failures: ArgoCD's repo-server fails
its own liveness probe and CrashLoops, the Prometheus operator and
kagent's kmcp-controller-manager lose their leader-election lease and
CrashLoop, and Grafana's readiness probe flaps, which nginx reports to
the browser as 503 (or 504 if the backend hangs instead of refusing).
Diagnose with:
1 2 3 4 5 6 7 8 9 10 11 | |
Fix:
1. Give Docker Desktop more CPU/memory (Settings → Resources) if the host has
headroom — 12 CPU / 24GB is comfortable for this stack.
2. local/kind-config.yaml runs a single worker node by default for this
reason; don't add more worker nodes locally unless you've also raised the
Docker Desktop allocation to match.
3. Re-run ./scripts/bootstrap-local.sh after freeing up resources — the
CrashLoop is usually self-healing once kube-apiserver stops timing out.
Symptom: /etc/hosts entries missing after bootstrap¶
The bootstrap writes hosts entries automatically, but may fail silently if /etc/hosts is read-only or if the script was interrupted.
1 2 3 4 5 6 7 8 | |
Symptom: Kind node NotReady after Docker crash/restart¶
This is covered fully in the runbook — see Kind Node IP Mismatch.
Short version: Docker reassigned the container IP; the kubelet cannot reach the API server with the old IP baked into /etc/kubernetes/kubelet.conf. Fix by updating the kubelet.conf or recreating the cluster.
Symptom: kube-proxy crash-loops, events show "too many open files" (Linux only)¶
Linux kernels have low default inotify limits that Kind clusters exhaust quickly.
1 2 3 4 5 6 7 8 9 10 | |
macOS users: this does not apply — the Docker VM manages its own kernel limits.
Symptom: Pod stuck in ImagePullBackOff¶
A scaffolded service was registered but its Docker image was never pushed to the local registry, or the cluster was recreated and registry contents were lost.
See ImagePullBackOff runbook for the full fix.
Short version:
1 2 3 4 | |
Phase 2 — Backstage¶
Symptom: Catalog is empty (0 entities) after Backstage starts¶
Most likely cause: The scaffolderActionsExtensionPoint was imported from the wrong path in a backend module, causing the scaffolder plugin to crash on startup. The crash aborts the catalog refresh loop, leaving final_entities empty even though refresh_state may have rows.
Check if this is the issue:
1 2 3 4 5 6 | |
If refresh_state > 0 but final_entities = 0: this is the scaffolder crash. The fix is already in the codebase — if you see this, your checkout may be on an older commit. Pull latest and rebuild.
1 2 3 | |
Second most likely cause: Backstage is still starting up (DB migrations run at startup). Wait 60–90 seconds and refresh.
Third cause: GITHUB_TOKEN is not set in local/.env. The catalog uses it to read entity files from GitHub. Without it, GitHub integration fails silently on many requests.
1 2 3 | |
Symptom: GitHub OAuth login fails / redirect loop¶
-
Check the OAuth app callback URL in GitHub — it must be exactly:
This is fixed by1http://backstage.idp.local/api/auth/github/handler/frameapp.baseUrlinbackstage/app-config.local.yaml(http://backstage.idp.local) — Backstage always generates this exactredirect_uriregardless of which host/port you loaded the page from, so there is nolocalhost:3000variant to register.Always open
http://backstage.idp.localin the browser, neverhttp://localhost:3000, even thoughdocker-compose.ymlalso publishes port 3000 directly. The GitHub sign-in flow uses a popup that posts a message back to the window that opened it; that handshake is keyed offapp.baseUrl, so if the app was loaded fromlocalhost:3000the popup (which always lands onbackstage.idp.local) is on a different origin than the opener and the sign-in silently never completes. You can sanity-check the redirect Backstage is sending without a browser:1 2
curl -sI http://backstage.idp.local/api/auth/github/start | grep -i location # Location header must show: redirect_uri=http%3A%2F%2Fbackstage.idp.local%2Fapi%2Fauth%2Fgithub%2Fhandler%2Fframe -
Check
local/backstage/.envhas bothAUTH_GITHUB_CLIENT_IDandAUTH_GITHUB_CLIENT_SECRETset. -
Restart Backstage after any
.envchange:Don't use1./scripts/bootstrap-local.sh --start-backstagedocker compose restart backstagehere —restartdoes not re-read.env, so a newly added/editedAUTH_GITHUB_CLIENT_ID/AUTH_GITHUB_CLIENT_SECRETis never picked up and login keeps failing. On the Kind provider, recreating the container can also change its IP on thekindDocker network, which--start-backstagere-wires automatically — see Backstage URL inaccessible after Docker restart below ifbackstage.idp.localstops responding after a restart.
Symptom: catalog-exporter CronJob in CrashLoopBackOff¶
This CronJob runs inside the cluster and tries to reach backstage.default.svc.cluster.local:3000. It fails whenever Backstage is not running. This is expected and harmless when Backstage is down.
Fix: start Backstage.
1 | |
Symptom: DORA metrics not in Grafana / Pushgateway empty¶
The dora-exporter CronJob runs every 15 minutes. Metrics will not appear immediately after bootstrap.
Trigger a manual run:
1 2 | |
If the job fails with python: can't open file '/scripts/dora-exporter.py', the ConfigMap was not populated:
1 2 3 | |
See also docs/local-setup.md — Troubleshooting observability for Kubernetes tab CPU/Memory issues.
Symptom: Catalog tables crash / blank on catalog, api-docs, or techdocs pages¶
This was caused by uuid v10 removing its default export, which broke @material-table/core. The fix (a yarn patch at .yarn/patches/) is committed to the repo and applied automatically by yarn install.
If you see TypeError: Cannot read properties of undefined (reading 'v4'):
1 2 3 4 | |
Then rebuild the image:
1 2 | |
Phase 3 — AI/ML stack (bootstrap-ai.sh)¶
Symptom: KAgent agents show READY=False¶
1 2 3 | |
Built-in agents may show READY=False briefly at startup while the controller reconciles. Restart the controller to clear stale conditions:
1 2 | |
Symptom: kagent.idp.local / idp-assistant.idp.local redirect to HTTPS, certificate error¶
Cause: Your browser cached HSTS for *.idp.local from a previous HTTPS setup.
Fix (Chrome):
1. Open chrome://net-internals/#hsts
2. Delete domain security policies for kagent.idp.local and idp-assistant.idp.local
3. Disable Settings → Privacy → Always use secure connections for local domains
4. Hard-reload (Cmd+Shift+R)
Fix (Firefox): Clear site data for the affected domains in Developer Tools → Storage → Clear.
Symptom: AI Assistant returns 502 / cannot connect¶
1 2 3 4 5 6 7 8 | |
The Backstage proxy target (/api/proxy/kagent) must point to http://idp-assistant.idp.local. Verify the ingress exists and the /etc/hosts entry is present.
Symptom: MCP server idp-mcp-server / qa-mcp-server not deployed on first install¶
On a first install, ArgoCD may not have registered the idp-services ApplicationSet before bootstrap-ai.sh runs. The script falls back to direct Helm, but if the fallback was interrupted, deploy manually:
1 2 3 4 5 6 7 | |
Phase 4 — AWS setup (bootstrap.sh)¶
For a comprehensive list of issues that were present in earlier versions and are now patched, see docs/DEPLOYMENT_GUIDE.md — Known Issues & Fixes. This section covers issues that can still occur on a fresh deployment.
Pre-flight: always run verify-secrets.sh first¶
1 2 | |
If it shows failures, fix them before running bootstrap.sh. The script checks AWS credentials, required env vars, GitHub token validity, and quota.
Terraform¶
For why each of these was possible — and which file now prevents it — see AWS install: known failure modes.
Symptom: terraform init fails — "Backend configuration required" or no such bucket¶
terraform/main.tf declares a partial backend — it holds no bucket name. Every
value comes from terraform/backend.hcl, which is gitignored and generated by
scripts/setup.sh (or by bootstrap.sh on first run) via ensure_tf_state_backend()
in scripts/lib.sh. That function also creates the bucket and the DynamoDB lock
table, because Terraform cannot provision the backend it stores its own state in.
The usual cause is running terraform init by hand before either script has run.
Fix by regenerating the file:
1 2 3 | |
To create it manually — bucket is <cluster-name>-terraform-state-<account-id>,
table is <cluster-name>-terraform-locks:
1 2 3 4 5 6 7 8 9 10 11 12 | |
Then terraform init -backend-config=backend.hcl — the bare terraform init will
prompt for every backend value.
Symptom: terraform init fails — DynamoDB lock table not found¶
ensure_tf_state_backend() creates both the S3 bucket and the DynamoDB table (see
the previous entry). If only the bucket was created manually — the table name is in
terraform/backend.hcl, not main.tf, which no longer holds any backend values:
1 2 3 4 5 6 7 8 | |
Symptom: Error acquiring the state lock — stale lock from interrupted run¶
1 2 3 | |
Symptom: Error: error creating EKS Node Group — NodeCreationFailure: Ec2SubnetInvalidConfiguration¶
AWS requires at least 2 subnets in different AZs for managed node groups. This triggers when your region has fewer than 2 AZs (rare) or if terraform.tfvars has availability_zones set to a single AZ.
1 2 3 | |
Symptom: EC2 quota exceeded — InsufficientInstanceCapacity or vCPU limit¶
The default quota for on-demand t3.medium is 32 vCPUs per region. 4 nodes × 2 vCPUs = 8 vCPUs, usually within quota. If not:
1 2 3 4 5 6 | |
Symptom: terraform destroy fails — ECR repos not empty / Crossplane resources blocking¶
Do not run terraform destroy directly. Use the cleanup script which handles dependency ordering:
1 | |
The cleanup runs 7 ordered phases: delete ALBs → disable RDS deletion protection → delete Crossplane-tagged resources → empty S3/ECR → terraform destroy → verify. Skipping this causes Terraform to fail on resource dependencies.
If cleanup.sh itself fails mid-run and you need to retry from a specific phase:
1 2 3 4 5 6 7 8 | |
EKS cluster¶
Symptom: EKS nodes stuck in NotReady¶
Wait 3 minutes — the IAM role binding propagates asynchronously. If it persists:
1 2 3 4 5 6 7 8 | |
Symptom: aws-load-balancer-controller pod crash-loops, ALBs not provisioning¶
1 | |
Common cause: the IRSA role (AWSLoadBalancerControllerIAMRole) trust policy does not match the OIDC provider URL. Terraform creates this automatically — if the trust policy is wrong, it means Terraform did not finish successfully. Re-run bootstrap.sh.
Symptom: ALB address stuck in <pending> for more than 10 minutes¶
1 2 3 4 5 6 7 | |
Normal wait: 3–5 minutes. If longer, the controller is likely not running or the IAM role is wrong.
Backstage (AWS)¶
Symptom: Backstage pod in CrashLoopBackOff or Error¶
1 2 | |
Common causes:
| Cause | Fix |
|---|---|
| ExternalSecret not synced | kubectl get externalsecret -n backstage — check READY=True |
| ClusterSecretStore not ready | kubectl get clustersecretstore — see below |
| RDS not ready | kubectl get pods -n backstage \| grep postgres |
| Config YAML parse error | Check logs for YAMLException — usually an indentation issue in the ConfigMap |
Symptom: ClusterSecretStore aws-secretsmanager shows InvalidProviderConfig¶
The External Secrets Operator's IRSA trust policy must match the ESO service account. Check:
1 2 | |
The service account annotation must match the IRSA role ARN. bootstrap.sh creates and annotates the SA automatically. If it was created by the Helm chart before the annotation was applied:
1 2 3 4 5 6 7 | |
Get the correct role ARN:
1 | |
Symptom: K8S_SERVICE_ACCOUNT_TOKEN expired — Backstage Kubernetes tab shows 401¶
EKS service account tokens expire. bootstrap.sh auto-populates the token, but it needs refreshing if the cluster was recreated or the token rolled:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
Symptom: GitHub OAuth login redirect loop after deployment¶
The OAuth app callback URL must be updated with the real ALB hostname (only known after bootstrap finishes):
kubectl get ingress backstage -n backstage -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'- Go to GitHub → Settings → Developer settings → OAuth Apps → your app → Edit
- Update Authorization callback URL to:
http://<ALB-HOSTNAME>/api/auth/github/handler/frame
Symptom: Apple Silicon Mac — pods show ImagePullBackOff / no match for platform in manifest¶
Images built on Apple Silicon (arm64) with docker build (no --platform flag) cannot run on EKS nodes (amd64). Always build for the target platform:
1 2 3 | |
Or use GitHub Actions CI (which runs on ubuntu-latest = amd64) to build and push automatically.
Observability (AWS)¶
Symptom: DORA metrics missing in Grafana¶
The dora-exporter CronJob runs every 15 minutes and requires a valid GITHUB_TOKEN in Secrets Manager. Trigger manually:
1 2 | |
If the job fails with an auth error, update the token in Secrets Manager (see GITHUB_TOKEN section in PRE_DEPLOYMENT_CHECKLIST.md).
Symptom: docker compose up fails with "required variable BACKSTAGE_AUTH_SECRET is missing a value"¶
Working as intended. That key signs Backstage's service-to-service tokens and has no default — the previous fallback was a literal published in this repository, so an unset value now stops startup instead of quietly signing with a shared key.
./scripts/setup.sh and ./scripts/bootstrap-local.sh both generate one into
local/backstage/.env. If you are starting compose directly on a tree that predates
this, fill it in once:
1 | |
Neither script overwrites an existing value — rotating it would sign every user out.
Symptom: Pushgateway has no data / QA metrics dashboard empty¶
Expected on a new install, and not a fault. The QA dashboard is no longer seeded with
sample values — it shows what the flaky-test exporter has actually collected, which is
nothing until a service publishes JUnit test-results* artifacts from its CI.
Check that the exporter is running and finding artifacts:
1 2 | |
A line reading 0/10 runs had artifacts means the repositories in the catalog are not
publishing test results yet, not that the exporter is broken.
Symptom: Grafana returning 503 intermittently¶
With a single node cluster, Grafana pod restarts can cause brief 503s. The nginx proxy is configured with proxy-next-upstream: http_503 so retries are automatic. If 503s persist more than 60 seconds:
1 2 3 | |
ArgoCD (AWS)¶
Symptom: idp-services ApplicationSet creates no applications¶
1 2 | |
Most common cause: aws/argocd/app-of-apps.yaml still has a moatazeldebsy placeholder (setup.sh was not run). Fix:
1 2 | |
Symptom: ArgoCD GitHub credentials expired — all apps show ComparisonError¶
1 2 3 4 5 6 7 | |
Symptom: Crossplane providers not healthy after bootstrap¶
Crossplane provider pods pull large images and may take 5–10 minutes on first install. Check:
1 2 3 4 5 6 | |
A failing provider pod usually means the IRSA role (CrossplaneAWSRole) is not yet propagated. Wait 2 minutes and check again.
bootstrap.sh interrupted mid-run¶
bootstrap.sh is idempotent for most phases. Re-run it after fixing the root cause:
1 | |
If only a specific phase failed, you can run individual pieces:
1 2 3 4 5 6 7 8 9 | |
Day-2 issues¶
Symptom: Backstage URL inaccessible after Docker restart¶
When Docker Desktop restarts, the Backstage container gets a new IP. The nginx ingress loses the upstream endpoint.
1 2 3 4 5 | |
Symptom: Scaffolded service not appearing in catalog after template runs¶
- Check the
catalog:registerstep output in the Backstage scaffolder UI — it should show the registered URL. - Check ArgoCD has synced the new service:
1kubectl get applications -n argocd | grep <service-name> - Force a catalog refresh in Backstage: Settings → Refresh entity.
- If the service repo's
catalog-info.yamlhas amoatazeldebsyplaceholder (old template version), re-runsetup.shand re-scaffold.
Symptom: helm upgrade fails — "release: already exists" or CRD conflict¶
1 2 3 4 5 6 7 8 | |
Getting help¶
Team Infrastructure (v0.4.0+)¶
Symptom: Crossplane claim rejected — "owner is required"¶
Kyverno validate policy crossplane-require-cost-tags is blocking the claim.
1 2 3 4 5 6 7 8 9 | |
If the policy is too strict for a specific namespace (e.g. during migration), temporarily
set validationFailureAction: Audit and remediate claims before re-enforcing.
Symptom: idp:team tag missing on Crossplane-provisioned AWS resource¶
- Check the claim namespace — mutation only runs in
team-*namespaces:1kubectl get s3bucket <name> -n <namespace> -o jsonpath='{.spec.parameters.team}' - Verify Kyverno is running:
1kubectl get deployment kyverno-admission-controller -n kyverno - Inspect Kyverno policy events:
1kubectl get policyreport -n <namespace> - If the claim was created before Kyverno was installed, delete and recreate it to trigger mutation.
Symptom: ExternalSecret error — "SecretStore not found: team--secrets"¶
The namespace-scoped SecretStore was not created (IAM role ARN was blank when the team was scaffolded).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
Symptom: all-templates.yaml not loading templates in Backstage catalog¶
The URL pointing to backstage/catalog/all-templates.yaml in app-config.aws.yaml must
resolve via the GitHub integration. Check:
- The
integrations.github.appsblock is configured (orGITHUB_TOKENis set as fallback) - The file was merged to
mainbranch before Backstage started - Force catalog refresh: Settings → Catalog → Refresh all
1 2 | |
Symptom: team=unknown on DORA Prometheus metrics¶
The team could not be resolved from TEAM_MAP or GitHub topics.
1 2 3 4 5 6 7 8 9 | |
Symptom: Grafana "Team — payments" folder not appearing¶
- Confirm the scaffold PR was merged and the ConfigMap exists:
1kubectl get cm -n monitoring -l grafana_folder | grep team - Check Grafana sidecar is running:
1 2
kubectl get pod -n monitoring -l app.kubernetes.io/name=grafana -o jsonpath='{.items[0].spec.containers[*].name}' # Should include: grafana-sc-dashboard - If sidecar is absent, re-upgrade Grafana with the sidecar values:
1 2
helm upgrade grafana grafana/grafana -n monitoring \ -f observability/grafana/grafana-helm-values.yaml --reuse-values
If an issue is not covered here:
- Check the docs/runbooks/ directory for alert-specific procedures.
- Check docs/local-setup.md for observability-specific troubleshooting.
- Check recent commits — many issues have been fixed:
git log --oneline --since="3 months ago" | grep fix. - File an issue at the GitHub repository.