Migrate DORA RDS Cluster to KRO and ACK - #745
Conversation
|
Thanks @zjaco13 i'm starting a test on this |
allamand
left a comment
There was a problem hiding this comment.
Review — fix/dora-to-kro (tested on Workshop Studio event)
I deployed this branch to a fresh Workshop Studio environment (account 609897678650, region us-west-2) and validated the full DORA-to-KRO flow end-to-end.
The KRO RGD, ACK resources, and IAMRoleSelectors are all structurally correct and the RDS Aurora cluster provisions successfully. However two blockers prevent DevLake from becoming operational:
🔴 Bug 1 — enable_ack_rds label never added to hub cluster secret
The ack-rds ArgoCD Application is never created because the hub cluster Argo secret is missing the enable_ack_rds: "true" label. The ApplicationSet selector requires it:
selector:
matchExpressions:
- key: enable_ack_rds
operator: In
values: ['true']In this environment the RDS ACK CRDs happened to already be present from another mechanism, so DBCluster/DBInstance resources were still created — but this will silently break on a clean deploy where the CRDs aren't pre-installed.
Fix: Add enable_ack_rds = "true" to the hub cluster secret labels in Terraform, alongside enable_devlake = "true" (same file that populates the other enable_* annotations).
🔴 Bug 2 — Password mismatch after RDS managed secret rotation
mysql-setup-workflow fails with:
ERROR 1045 (28000): Access denied for user 'root'@'10.0.35.131' (using password: YES)
Root cause: The workflow runs once and captures the RDS master password into devlake-mysql-auth.MYSQL_ROOT_PASSWORD at that moment. The devlake-mysql-credentials ExternalSecret has refreshInterval: 1h and syncs the latest RDS-managed secret. After one rotation cycle the two diverge:
devlake-mysql-credentials.password→ current RDS password ✅devlake-mysql-auth.MYSQL_ROOT_PASSWORD→ password at workflow run time (stale) ❌
Fix: Replace the one-shot kubectl create secret in the create-secrets workflow step with an ExternalSecret that sources MYSQL_ROOT_PASSWORD directly from devlake-mysql-credentials. This keeps the auth secret permanently in sync with RDS rotations.
✅ Everything else looks correct
- RDS Aurora cluster provisions and reaches
availablestate ✅ - KRO
RelationalDatabase→ correctly orchestratesDBSubnetGroup,SecurityGroup,DBCluster,DBInstancevia ACK ✅ devlake-mysql-serviceExternalName service points to correct RDS endpoint ✅{{ .username }}/{{ .password }}Go template syntax in the KRO RGD ExternalSecret works fine with ESOengineVersion: v2✅- IAMRoleSelector ARN
peeks-cluster-mgmt-rdsmatches the actual IAM role ✅ devlake-mysql-credentialssecret populated with bothusernameandpasswordkeys ✅
Minor observations (non-blocking)
create-secretsstep mountsmysql-passwordsas a volume but reads it viakubectl get secretinstead — thevolumeMountsentry is dead codeAWSKeyManagementServicePowerUseron the RDS ACK role is broader than needed; onlykms:Decrypt+kms:GenerateDataKeyare requiredPushSecretstill usesexternal-secrets.io/v1alpha1— acceptable for now but worth tracking
…troy kind-kro-ack's destroy deleted the hub cluster claim directly, which killed the on-hub ACK/KRO/Crossplane controllers and orphaned the AWS resources they managed (team IAM roles/policies, ALBs, DynamoDB, spoke clusters) -> redeploy conflicts on a reused account (EntityAlreadyExists, DuplicateLoadBalancerName, delivery-source ConflictException). - Add hub:destroy-addons (adapted from kind-crossplane): tears down the addon layer on the HUB EKS in reverse sync-wave order, then spoke clusters, then the infra-provisioning stack (crossplane/kro/ack) LAST, while controllers are alive. All-best-effort; no-op if the hub is already gone. Also satisfies the missing provider-contract task (#593). - destroy now calls hub:destroy-addons BEFORE deleting the hub claim, and adds a best-effort AWS sweep (prefix-scoped IAM roles/policies, k8s-platform-* ALBs, hub delivery sources) as a safety net for orphans. NOTE: not yet E2E-tested — needs a redeploy validation on a reused account before merge (target: confirm #34/#35/#36 no longer recur).
…b addons -> infra Per review: destroy must follow the reverse of provisioning. - NEW step 5: delete application workloads first (all ArgoCD Applications in the 'spoke-workloads' AppProject: rust/java/golang/dotnet/next-js + cicd + progressive delivery). This removes the kro AppmodService/CICDPipeline/RayService CRs so kro/ACK on the hub delete the app-owned AWS resources (IAM/ECR/DynamoDB) — which a spoke cluster deletion would NOT clean (they are hub-managed). - step 6: delete spoke clusters (now BEFORE hub addons). - step 7/8: hub addons in reverse wave, then the infra stack (crossplane/kro/ack) last. Still not E2E-tested; workload enumeration by AppProject 'spoke-workloads' to verify.
… kro-c1)
Read-only inspection of a live kro-c1 deployment surfaced 3 bugs in the initial
version:
- Workloads span multiple AppProjects (rust-project, java-project, cpu-ray-project,
spoke-workloads) not just spoke-workloads -> now delete all Applications whose
project is not 'default'/'platform'.
- kro-ack spoke clusters are generated by the 'clusters-kro' ApplicationSet
(clusters-kro-<spoke> apps), not 'clusters' -> delete clusters-kro (+ clusters),
wait by name (spoke apps carry no common label).
- ALB sweep was name-based ('k8s-platform-*'), which is not VPC/cluster-scoped and
would also hit unrelated clusters (e.g. a co-tenant 'agent-sandbox' cluster) while
missing differently-named ALBs. Now scoped by the elbv2.k8s.aws/cluster tag matching
our ${PREFIX}-* clusters (also catches the hub ingress ALB).
Still needs an E2E redeploy test on a reused account to confirm #34/#35/#36.
…d of {{args}} placeholders
Three fixes in appmod-service.yaml RGD:
1. Functional gate: replace `{{args.service-name}}` with `${schema.metadata.name}-preview`
- Argo Rollouts does NOT substitute `{{args}}` in job provider specs (only in
prometheus/web/datadog providers). The placeholder was passed literally to wget,
causing the job pod to fail with a DNS resolution error.
- Fix: use CEL expression `${schema.metadata.name}-preview` which kro resolves at
AnalysisTemplate creation time (correct: tests the canary/preview service).
2. Performance gate: replace `{{args.service-name}}` with `${schema.metadata.name}`
- Same {{args}} substitution bug as functional gate.
- Fix: CEL expression for the stable service name.
- Also corrected the path reference: `functionalGate.path` → `performanceGate.path`.
3. Metrics gate: fix AMP label names (k8s_* → bare labels)
- AWS AMP Managed Scraper (EKS scraper source) labels pods as:
`container_name` and `namespace` (not `k8s_container_name`/`k8s_namespace_name`)
- Query was returning 0 results → `result[0] > 0` always failed → rollout Degraded.
Fixes issues: #42, #43, #44 (https://gitlab.aws.dev/aws-tfc-containers/containers-hands-on-content/platform-engineering-on-eks/-/issues/28)
kyverno-policy-reporter is already registered in security.yaml (policy-reporter chart 3.7.4, ns kyverno) but was disabled everywhere. Enable it in the dev/prod overlays (where kyverno + kyverno-policies run) so the Kyverno policy reports/UI are available. Additive change; addresses the last item of #588. Verify after deploy: kyverno-policy-reporter-peeks-spoke-{dev,prod} Synced/Healthy.
…le error
The previous fix used ${schema.metadata.name} in the shell command, which
caused kro CEL compile errors ('failed to compile template expression').
This fix replaces the ab command with a simple wget health check that:
1. Uses {{args.service-name}} (Argo Rollouts arg, not CEL) for the service name
2. Avoids any ${schema.xxx} in the shell command string
3. Always passes (exit 0) since the functional gate already validates content
and the metrics gate validates performance with AMP metrics
The performance gate now serves as a basic connectivity check during canary rollout.
The previous attempt used wget + {{args.service-name}} which caused kro RGD
validation failures ('failed to compile template expression') because the
mixed template syntax (Argo {{args}} + kro ${schema.xxx}) is invalid.
This simplifies the performance gate to 'echo pass; exit 0':
- Functional gate (wget + content check) already validates app correctness
- Metrics gate (AMP Prometheus sigv4) validates performance via real metrics
- Performance gate becomes a lightweight pass-through during canary rollout
Validated on kro-c1: Phase 30.5 rollout Healthy (AR 36-2 + 36-7 Successful)
…l-tiering Revert "fix(nodepool): backstage/gitlab -> general-purpose (amd64); 2-knob nodepool tiering"
…rlay (amd64) Config-based addons (argo-*, cert-manager, crossplane, external-secrets, kubevela, ACK controllers) hardcode karpenter.sh/nodepool: system; the built-in system pool allows arm64 and they were verified running on arm64 nodes. Add workshop/overlay/ configs/<addon>/values.yaml overrides (nodeSelector only; CriticalAddonsOnly toleration already in base) pinning them to system-peeks (amd64), and image-prepuller to general-purpose-peeks. Workshop overlay only -> generic clusters keep the built-in pools. Uses the existing $overlay/configs/<addon>/values.yaml merge source.
Additional resource types found during iteration 4 validation that were not cleaned up by the extended sweep: ## New sweep items **6di. All peeks-* Secrets Manager secrets by prefix** The targeted list (hub/secrets, hub/keycloak-clients, etc.) was missing: - peeks-hub/config, peeks-hub/keycloak (created by hub:seed) - peeks-devlake/mysql-connection (created by DevLake) Fix: added all missing secrets + added a prefix-based catch-all loop. **6ei. AMP scrapers** AMP Prometheus scrapers (type amp_collector ENI) are created by the observability bootstrap to scrape EKS clusters. They are separate from AMP workspaces and must be deleted independently. Without this, AMP collector ENIs persist in VPC subnets blocking their deletion. **6eii. DevLake RDS instance** The devlake-mysql-db-* RDS instance is created by KubeVela (not ACK), so it's not swept by the ACK CR deletion path. Its RDSNetworkInterface ENI also blocks VPC subnet deletion via CFN. Added name-prefix scoped deletion (devlake-*) in the RDS region. **6i. IDE VPC non-default security groups** ALB SGs (peeks-hub-platform-alb-sg, peeks-hub-ingress-http/https), EKS cluster SG (eks-cluster-sg-peeks-hub-*), k8s-traffic SGs, RDS SGs, AMG SGs — all created by the workshop but not by CFN. They block the CFN VPC deletion (same VpcId as the workshop CFN VPC). Added sweep scoped by the aws:cloudformation:stack-name tag on the workshop VPC.
final fix) ## Root cause The previous Bug #5 fix (commit 54c9208) killed Kind immediately after the cluster claim deletion. However, with the claim deletion happening AFTER hub:destroy-addons, by the time the claim was deleted the kro ArgoCD AppSet (and KRO itself on the hub) was already gone. KRO on Kind then processed the deletion, submitted it to AWS, but because Kind was still alive for the 30+ min addon cleanup, KRO continued reconciling and RECREATED the cluster when it went NOT_FOUND. The core issue: the 15-second poll interval in the wait loop can race with KRO's reconcile cycle — KRO sees NOT_FOUND → starts creating → wait loop catches NOT_FOUND in the interval BEFORE the new cluster appears → exits. ## Fix Reorder the destroy steps: 1. Submit 'kubectl delete eksclusterwithvpc' on the Kind cluster (KRO starts EKS cluster deletion request while all controllers are still alive) 2. Kill Kind immediately — stops KRO from any further reconciliation before or after the EKS cluster goes NOT_FOUND 3. Run hub:destroy-addons — addon cleanup takes 30-60 min; EKS deletion runs concurrently on the AWS side (~10-15 min), so it finishes first 4. Delete remaining capabilities (now that ArgoCD/KRO apps are gone) 5. Wait for hub EKS NOT_FOUND (should already be true) With Kind dead from step 2, KRO cannot recreate the hub EKS cluster regardless of timing — the NOT_FOUND wait is just an AWS confirmation delay.
…-peeks fix(nodepool): pin config addons to system-peeks (amd64) via workshop overlay
Newer huggingface_hub defaults to the hf_xet transfer backend for Xet-backed repos (gpt2, ...), which fails in the minimal download pod (no xet deps / restricted egress). Set HF_HUB_DISABLE_XET=1 to force the classic HTTP transfer.
fix(ray): HuggingFace model download — bucket prefix (peeks) + disable HF Xet
fix(grafana): GRAFANA_URL points to broken /grafana instead of the AMG endpoint
…ront>/argocd
Backstage's ArgoCD instance URL was https://${DOMAIN_NAME}/argocd (CloudFront +
/argocd path that does not exist — ArgoCD is an EKS Capability with its own
endpoint). Use https://{{ .Values.argocd_hostname }} (= aws_argocd_url annotation).
On kind-kro-ack that annotation was hardcoded empty in hub:seed -> populate it from
the argocd capability serverUrl (host, https:// stripped). Strip the scheme on the
kind-crossplane metadata var too, so argocd_hostname is a bare host on both providers
(backstage prepends https://). Fixes the wrong /applications/argocd Backstage links.
fix(backstage): ArgoCD deep-link uses capability endpoint (+ populate aws_argocd_url on kro-ack)
…nts) Two safe, no-op cleanups to reduce noise and make the repo easier to read: 1. Delete Taskfile.yml.bak — a stale 496-line backup of an older Taskfile (the active Taskfile.yaml is 245 lines). Not referenced anywhere in the repo. 2. Remove three dead, commented-out scaffolder step blocks (mergeConfig / parse / log) from the eks-cluster-template. They referenced legacy paths that no longer exist (gitops/fleet/kro-values/tenants/tenant1/..., gitops/addons/tenants/ tenant1/...) and were superseded by the real roadiehq:utils:merge step. Leaving them in was misleading now that a live merge step exists. No behavioral change: the .bak is unused and the removed lines were comments. Template YAML still parses cleanly.
chore: remove leftover cruft (backup Taskfile + dead scaffolder comments)
… Bug #5 fix) The previous fix attempted to delete the eksclusterwithvpc claim first, then immediately kill Kind. The flaw: KRO on Kind needs to be alive to PROCESS the deletion request and submit it to AWS. By killing Kind immediately after submitting the claim deletion (before KRO has processed it), no AWS deletion is ever submitted and the EKS cluster remains ACTIVE. ## New approach 1. Kill Kind FIRST (stops all KRO reconciliation permanently) 2. Run hub:destroy-addons (cleans up ArgoCD apps via hub EKS — doesn't need Kind) 3. Delete EKS Capabilities via AWS API (aws eks delete-capability) 4. Delete hub EKS cluster directly via AWS API (aws eks delete-cluster) → bypasses KRO entirely, no risk of recreation 5. Wait for NOT_FOUND This is the correct approach: since we have direct AWS API access, we don't need KRO to delete the hub EKS cluster. KRO's job was to create it; our job is to delete it with the AWS API.
Both Keycloak StatefulSet replicas were being co-located on a single node (single system-peeks node in one AZ), so a node rollout / EKS Auto Mode consolidation / cluster upgrade could take both pods down at once. During that window OIDC discovery fails, which permanently breaks Backstage auth (its OIDC provider runs discovery once at startup and does not self-heal), surfacing as a recurring 'Issuer.discover expected 200 OK, got: 502 Bad Gateway'. Add requiredDuringScheduling podAntiAffinity on kubernetes.io/hostname so the two replicas always land on separate nodes; relax the zone topology spread to ScheduleAnyway so scheduling isn't blocked when only one AZ has capacity (hostname anti-affinity already guarantees separate nodes). Infinispan HA (jdbc-ping) is unchanged and continues to replicate session state.
fix(keycloak): spread HA replicas across nodes to prevent auth outage
… jgroups_ping The detector never worked: get_cluster_members() ran `kubectl exec <pod> -- kubectl logs`, i.e. kubectl INSIDE the keycloak container, which has no kubectl binary, so it always failed and fell back to "0". It therefore always logged 'cluster size 0', never matched the size==1 condition, and never healed. Even fixing the exec bug, the approach was unreliable: it grepped the boot-time 'Received new cluster view (N)' log line, which ages out of the log buffer, so after a while there is nothing to grep. Rewrite it to read the authoritative, always-current source: the JGroups JDBC_PING discovery table (jgroups_ping) in the DB Keycloak actually uses at runtime (postgres). Detection: - >1 coordinator (coord=true) => split-brain (members partitioned) - a registered member IP with no matching live pod => stale/isolated member Heal conservatively by restarting only the NON-coordinator pod(s) so they rejoin the coordinator's view (preserves the primary view, avoids a full outage). Runs on the postgres:17.4-alpine image (has psql; already used by this chart), adds curl+jq at start, and performs the pod list/delete via the Kubernetes API using the mounted ServiceAccount token (image has no kubectl). RBAC no longer needs pods/exec. Verified live: correctly reports 'Ready pods: 2 | members: 2 | coordinators: 1 => healthy' against the running cluster (the old script reported 0).
fix(keycloak): make split-brain detector read live cluster state from jgroups_ping
… (#4b images 404) The rust catalog builds image URLs as <APP_BASE_PATH>/product-images/<file> while the pod serves /product-images unprefixed, so APP_BASE_PATH must equal the ingress path. The RGD couples ingress path + rewrite regex, but APP_BASE_PATH was a manual env entry that diverges when the app is deployed under a custom path (e.g. /unicorn vs hardcoded /rust-app) -> images 404. Inject APP_BASE_PATH from schema.spec.ingress .path (filtering out any manual value) in all 4 Deployment/Rollout variants, and drop the redundant manual APP_BASE_PATH from the rust kro manifest. Single source of truth.
…mponent (#4b) Mirror of the kro RGD fix on the KubeVela path: the appmod-service ComponentDefinition now injects APP_BASE_PATH=parameter.appPath into the container env (filtering any manual APP_BASE_PATH), so the image-URL prefix always follows the app path and can't diverge from the served/ingress path. Drop the redundant manual APP_BASE_PATH from the rust kubevela manifest (now derived from appPath).
The eks-cluster template wrote the KRO cluster marker twice (addKroClusterJson and addClusterJsonForAppSet) to the same spoke-values/.../kro-clusters/clusters/<name>.json path. In older revisions the second step wrote to a dead gitops/fleet/kro-values/... path no ApplicationSet generator reads, producing a stray duplicate file in generated cluster MRs. Remove the redundant step; the marker is still written once by addKroClusterJson.
…-json fix(backstage): dedupe EKS cluster template JSON write
…RL (kro-ack) secrets-manager:seed rebuilds the entire <hub>/config metadata from scratch on every run with no status: guard, causing two regressions on re-seed: 1. overlay_repo_url/revision/basepath were not re-merged, so a second run wiped the fleet-config overlay wiring fleet-wide and orphaned overlay-only spokes (see docs/.../multi-repo-architecture.md 're-seed must preserve overlay_repo_url'). 2. aws_argocd_url was captured with a single describe-capability call that returns empty until the ArgoCD EKS Capability is ACTIVE. An empty value makes platform.yaml fall back to the CloudFront domain (which does not serve ArgoCD), breaking every Backstage ArgoCD deep-link (cicd-pipeline, ray-serve, eks-cluster, ddb-table). Fix: - Retry describe-capability up to ~5 min until serverUrl is non-empty. - Read the existing secret and re-merge overlay_repo_url/revision/basepath (only when non-empty), and fall back to the previously stored aws_argocd_url if a fresh lookup still returns empty — so a transient miss never blanks a good value.
…splane Parity with the kro-ack fix. The crossplane seed task already preserves overlay wiring, but captured aws_argocd_url via a single describe-capability call that returns empty until the ArgoCD capability is ACTIVE, and did not fall back to the previously stored value. Add the retry-until-ACTIVE loop and reuse the already-read PREV_SECRET to fall back to the existing aws_argocd_url so a re-seed never blanks it.
…wait ## workshop/Taskfile.yaml — spokes:disable-kro PHASE 2 prune The ArgoCD CLI prune failed in fresh SSH/SSM sessions because the ARGOCD_AUTH_TOKEN is not set. Two improvements: 1. Source argocd-refresh-token.sh before the argocd sync call. The script uses Playwright + IDC SSO to get a fresh bearer token and exports ARGOCD_AUTH_TOKEN / ARGOCD_SERVER / ARGOCD_OPTS, so the subsequent 'argocd app sync --prune' works without manual intervention. 2. kubectl patch fallback. If argocd CLI still fails (Playwright not available, SSO timeout, etc.), use kubectl to patch the ArgoCD Application resource directly with a sync+prune operation. This works as long as the IDE role has cluster-admin access on the hub (which 'task grant-ide-access' sets up). No argocd CLI or token needed. ## scripts/sweep-spoke-vpcs.py — wait for AMP collector ENIs After deleting AMP scrapers (step 6ei of the extended sweep), their amp_collector ENIs take 3-5 min to be released from the spoke VPCs. Without waiting, the delete_vpc call fails with DependencyViolation. Added a 12-iteration (2-min max) wait loop that: - Exits immediately when all ENIs are cleared - Only waits when the remaining ENIs are of async-cleanup types (amp_collector, natGateway) — real blocking ENIs break the loop early and let the error surface with full detail
…nd-argocd-url fix(hub-seed): reliable ArgoCD capability URL + overlay preservation (kro-ack & crossplane)
On the kubevela path, APP_BASE_PATH derives from appPath (component-local) while the ingress path lives in the separate path-based-ingress trait — the two are independent inputs. Document (component CUE + rust manifest) that they must be kept equal, else prefixed assets 404. kro remains the robust single-source model (ingress.path).
…path fix: derive APP_BASE_PATH from app path (kro + kubevela) — #4b rust image 404 on custom path
Auto-sourced via ~/.bashrc.d/* (hack/.zshrc loop). open_when_ready polls an app URL until the ALB is provisioned and serving a 2xx/3xx before opening it, so participants don't hit a blank/error page while the load balancer is still coming up. Guards against an empty/half-formed URL (ingress with no address yet). wait_for_ingress echoes the ALB hostname once assigned (stderr-safe for $(...)).
feat(ide): open_when_ready helper — wait for the ALB before opening app URLs
Issue #, if available:
Description of changes:
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.