Eagle Eye + Hunger Games — Migrate Staging from DOKS/DOCR to AWS EKS/ECR
Status: planning — not yet started. Epic issues: eagle-eye#174, hunger-games#32.
Why
Both apps currently ship staging to DigitalOcean Kubernetes (DOKS) with images in DOCR. Two motivations for the move:
- Cost & cluster consolidation — most other HungryHub services already run on
eks-dev-262in thesandboxaccount. Migrating eagle-eye and hunger-games staging means a single staging plane to operate. - Path to production — production already runs on
eks-prod-21. Having dev/staging on the same EKS flavour means a single mental model, one set of RBAC + IAM plumbing, one set of OIDC trust relationships.
Production (eks-prod-21, GHCR) is out of scope for this document. We are not touching the prod pipeline for either app.
Current state (verified 2026-06-11)
| Eagle Eye | Hunger Games | |
|---|---|---|
| Staging cluster | DOKS (do-sgp1-staging) | DOKS (do-sgp1-staging) |
| Staging registry | DOCR | DOCR |
| Production cluster | AWS EKS eks-prod-21 (ap-southeast-1, account 202255947274) | AWS EKS eks-prod-21 (ap-southeast-1, account 202255947274) |
| Production registry | GHCR | GHCR |
| Trigger (auto) | push to main → staging | push to main → prod ⚠️ (fixed by #34) |
| Trigger (manual) | workflow_dispatch → pick env | workflow_dispatch → pick env |
Confirmed by repo maintainer (2026-06-11): the intended trigger model for hunger-games is
push to main → staging, withworkflow_dispatchfor prod. The auto-prod bug is unintended.
eks-dev-262 namespaces inspected — no eagle-eye or hunger-games namespace yet.
sandbox ECR — no eagle-eye-* or hunger-games-* repos yet.
GitHub OIDC provider token.actions.githubusercontent.com is already in the sandbox account (owned by hungryhub-terraform/oidc-iam-rule/). We reuse it; we do not create a second one.
Target state
| Eagle Eye | Hunger Games | |
|---|---|---|
| Staging cluster | AWS EKS eks-dev-262 (ap-southeast-1, account 079994049689) | AWS EKS eks-dev-262 |
| Staging registry | AWS ECR, account 079994049689 | AWS ECR, account 079994049689 |
| Namespace | eagle-eye (shared with eagle-eye app, by design) | eagle-eye (same, on EKS) |
| Trigger (auto) | push to main → EKS staging (no change in trigger; new target) | push to main → EKS staging (was: prod; fix in #34) |
| Production | unchanged (EKS prod, GHCR) | unchanged (EKS prod, GHCR) |
Old DOKS staging clusters and DOCR images are decommissioned after a 1–2 week observation window. The DOCR namespaces are drained, not deleted on day 1.
ECR repository layout
One repo per image, prefixed by service. Retention policy applied to each.
| Repo | Source |
|---|---|
eagle-eye-frontend-server | _app_services/frontend-app/server/Dockerfile |
eagle-eye-frontend-worker | _app_services/frontend-app/worker/Dockerfile |
eagle-eye-frontend-client | _app_services/frontend-app/client/Dockerfile |
eagle-eye-go-api | _app_services/go-service/deploy/docker/api/Dockerfile |
eagle-eye-go-worker | _app_services/go-service/deploy/docker/worker/Dockerfile |
eagle-eye-go-scheduler | _app_services/go-service/deploy/docker/scheduler/Dockerfile |
hunger-games-api | deploy/docker/api/Dockerfile |
hunger-games-worker | deploy/docker/worker/Dockerfile |
ECR lifecycle policy (applies to all of the above)
“Keep last 2 months” — codified as the following rules, evaluated in priority order. Untagged images (failed builds, intermediate layers) are cleared after 7 days to bound storage. Tagged images are kept up to 60 days OR the most recent 50, whichever expires first. Always-preserved tags (*-latest, semver-like vX.Y.Z) are exempt.
[
{
"rulePriority": 1,
"description": "Expire untagged images after 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Keep at least 50 tagged images (covers bursty build cadence)",
"selection": {
"tagStatus": "tagged",
"countType": "imageCountMoreThan",
"countNumber": 50
},
"action": { "type": "expire" }
},
{
"rulePriority": 3,
"description": "Expire tagged images after 60 days (~2 months)",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["stg-", "prod-"],
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 60
},
"action": { "type": "expire" }
},
{
"rulePriority": 4,
"description": "Always preserve floating tags and semver tags",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["staging-latest", "production-latest", "v"],
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 365
},
"action": { "type": "expire" }
}
]
Rules are evaluated in order — rule 4 keeps a long tail, so the 60-day cap in rule 3 only applies to non-protected prefixes.
IAM & OIDC (per repo)
Per hungryhub-iam/AGENTS.md rules, OIDC subject patterns are pinned and prod is isolated:
| Role | Repo | Subject pattern | Inline policy |
|---|---|---|---|
github-actions-eagle-eye-staging | hungryhub-team/eagle-eye | repo:hungryhub-team/eagle-eye:ref:refs/heads/main, repo:hungryhub-team/eagle-eye:pull_request | eks:DescribeCluster, ecr:GetAuthorizationToken, ecr:BatchCheckLayerAvailability, ecr:PutImage, ecr:InitiateLayerUpload, ecr:UploadLayerPart, ecr:CompleteLayerUpload (scoped to the 6 ECR repos), secretsmanager:GetSecretValue (scoped to specific secret ARNs), sts:AssumeRole for the cluster’s k8s role (via aws-auth ConfigMap) |
github-actions-hunger-games-staging | hungryhub-team/hunger-games | same shape, hunger-games | same shape, scoped to the 2 ECR repos |
ARN scoping follows hungryhub-iam/AGENTS.md §3.4 — no Resource = "*".
The OIDC provider (arn:aws:iam::079994049689:oidc-provider/token.actions.githubusercontent.com) is not recreated. We add the new roles in a new stack stacks/eagle-eye-hunger-games-staging/ under hungryhub-iam, using the existing modules/github_oidc_role/ module.
Bootstrap order
-
Infra (manual / IaC):
- Add
stacks/eagle-eye-hunger-games-staging/tohungryhub-iamwith the two OIDC roles and their scoped inline policies. Apply againstsandbox. - Create the 8 ECR repos in
sandbox(ap-southeast-1) with the lifecycle policy above. Either via a new Terraform stack inhungryhub-terraform(ecr/eagle-eye-hunger-games-staging/) or viaaws ecr create-repositoryfor now and codified in a follow-up. Decide before the PR. - Create the
eagle-eyenamespace oneks-dev-262(no separatehunger-gamesnamespace — the two apps share by design, see Eagle Eye + Hunger Games namespace note below). No special RBAC needed; namespaces are cheap. - Add the GitHub Actions role ARNs to the cluster’s
aws-authConfigMap so the CI role can calleks:DescribeClusterand write to the namespace. This is a one-timekubectl edit cm aws-auth -n kube-systemplus a restart of the node groups (or useeksctl create iamidentitymapping).
- Add
-
Workflow changes (eagle-eye):
- Add new GitHub Environment
eks-stagingand mirror all existingSTAGING_*secrets into it, OR renamestaging→eks-stagingand add anstagingalias. Decision: rename toeks-stagingto make the cluster name explicit, and remove the oldstagingenv once the rename ships. - Update
.github/workflows/deploy.yml:validatejob: drop theKUBECONFIG_DO_STAGINGrequirement, addAWS_ROLE_TO_ASSUME_STAGINGandAWS_EKS_CLUSTER_NAME_STAGINGrequirements.build-imagesmatrix: change the registry prefix tosandbox ECR registryfor staging runs.deployjob: when environment ==eks-staging, assume the OIDC role andaws eks update-kubeconfig(mirroring the existing prod path).
- Update
deploy/k8s/staging/manifests if they have any DOKS-specific annotations or service types.
- Add new GitHub Environment
-
Workflow changes (hunger-games):
- Same shape of changes as eagle-eye.
- Prerequisite: remove auto-deploy to prod on push to
main(issue #34). Without this, the new staging deploy can race with prod.
-
Cutover (both apps):
- Land infra + workflow PRs in dry-run mode first.
- Manually trigger a staging deploy on each app via
workflow_dispatch. Watchkubectl get pods -n <ns>until all 5–9 deployments are healthy. - Sanity-check the eagle-eye frontend (
<env>-latesttag) renders, API responds, scheduler runs, BullMQ workers consume. - Repeat for hunger-games.
- Leave DOKS staging running for 1–2 weeks. If no rollback needed, drain workloads and delete the DOKS clusters via DigitalOcean control panel.
-
Cleanup (post-cutover):
- Delete the DOKS clusters.
- Remove the legacy
cleanup-registries.ymllogic for DOCR namespaces of these two apps. - Update
Domain_and_Namespace_List.mdto list theeagle-eyenamespace oneks-dev-262(it’s a shared namespace hosting both apps, not a new namespace). - Update
Eagle_Eye_Architecture_and_Operations.mdandHunger_Games_Architecture_and_Operations.mdto reflect the new cluster/registry.
Why both apps share the eagle-eye namespace
Verified 2026-06-11 against prod EKS (eks-prod-21) and DOKS staging: both namespaces host both apps’ deployments. Eagle Eye’s BFF calls the hunger-games API over same-namespace DNS using HUNGERGAMES_API_BASE_URL (visible in the eagle-eye-env secret on prod). Splitting them would require a cross-namespace NetworkPolicy and DNS rewrite and is out of scope for the staging migration.
The hard-coded NAMESPACE=eagle-eye in hunger-games/.github/workflows/deploy-kubernetes.yml:260 is therefore correct, not a copy-paste bug. (An earlier version of this plan and epic #33 incorrectly claimed otherwise; both have been corrected.)
Rollback
- Before cutover: revert the workflow PR. No infra destroyed; nothing to undo.
- After cutover, within 2 weeks: re-point the workflow at DOKS (the old kubeconfig + DOCR credentials are still valid). DOKS clusters kept running for this reason.
- After DOKS decommission: restore from ECR. Tagged images are kept for 60 days (rule 3); if rollback is needed past that, the manifests in
deploy/k8s/<env>/are the source of truth for reconstruction.
Cross-cutting design decisions to revisit after the migration
These are the open issues this plan is not solving — file separately:
- Image-baked CA bundle (eagle-eye prod). Stale CA risk. Plan: move to AWS Secrets Manager + job-time fetch.
- Hunger-games maturity — auto-prod on
main+ wrong namespace. Tracked separately. - Drift detection on K8s secrets vs GitHub Environment secrets. Tracked separately.
Open questions for the team
- Are the eagle-eye and hunger-games repos OK being added as new stacks in
hungryhub-iam, or should they live in their own repo? Default: new stack inhungryhub-iamto follow the existing pattern. - Do we want a single shared ECR registry prefix (
eagle-eye-*andhunger-games-*repos under the same registry), or separate registries? Default: single registry, prefixed repo names. Cheaper and simpler. - Should the eagle-eye and hunger-games IAM stacks be one stack or two? Default: one stack, since they share the OIDC provider and apply together.