DataTalksClub / DataTalksClub/website
Migrate legacy development Terraform, state, and OIDC identifiers safely
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
Normative specifications: 08 — AWS development and Terraform, 09 — migration and rollout, 10 — verification strategy, and the development legacy-identifier boundary. Backend behavior follows HashiCorp's terraform init and S3 backend/native-lock contracts.
Product decision
https://web.dtcdev.click is the existing website development environment. This issue finishes the live infrastructure part of #93 in place; it does not create another deployment.
Only identifiers that can move without replacing the serving stack will move:
- canonical Terraform source root:
sandbox/website→development/website; - S3 backend key:
sandbox/website/terraform.tfstate→development/website/terraform.tfstate, with native lock changing to the corresponding.tflockkey; - GitHub application deployment environment:
sandbox→development; and - deployer OIDC subject:
repo:DataTalksClub@72699292/website@1326548167:environment:sandbox→repo:DataTalksClub@72699292/website@1326548167:environment:developmentthrough a bounded dual-trust transition.
Physical AWS identifiers whose rename would replace a resource, split data/history, change an endpoint, or interrupt delivery are permanently retained as registered legacy development compatibility identifiers. They are not current product terminology. This includes the shared state bucket, website-sandbox* resource names/ARNs, secret paths, log groups, KMS alias, IAM role names, ECS cluster/services/task families, RDS identifier, ECR repository, ALB/target-group/security-group/VPC names, and Environment=sandbox resource/release tag. The hostname, account, region, topology, data, active image, desired counts, and application behavior remain unchanged.
Production accounts, infrastructure, state, trust, DNS, data, and deployment are entirely out of scope.
Authoritative starting inventory
The PM refreshed the non-secret inventory on 2026-08-09 without Terraform/AWS/GitHub mutation:
- website and
origin/mainare3f6c227b51bee8eabb09a89955603193574d7090; automatic run 31298390668 is terminal green and/health/livereports that exact SHA;/health/readyand/return200with valid TLS andnoindex, nofollow; DataTalksClub/aws-inframain is95d93f7e07ded19e482a0c6d6471fbd93fb608d8; the executable root issandbox/website, callsmodule "website", and uses Terraform>=1.10with S3use_lockfile=true;- the accepted state inventory contains exactly 98 addresses (91 managed resources plus seven data addresses), in bucket
datamailer-sandbox-817685572750-us-east-1-tfstate, keysandbox/website/terraform.tfstate; the accepted native lock is absent; - the website repository has exactly seven repository-scoped
DEVELOPMENT_*variables, includingDEVELOPMENT_AUTO_DEPLOY=true, and thesandboxenvironment has exactly 18 environment-scopedDEVELOPMENT_*variables and zero environment secrets; - the only website deployment environment is
sandbox; it has exactly one custom deployment branch policy,main;developmentanddevdo not exist; and - the current workflow contains exactly four literal
environment.name: sandboxbindings. The publisher continues to use the exact immutable main-ref subject. The physical publisher/deployer roles remainwebsite-sandbox-github-publisherandwebsite-sandbox-github-deployer.
This inventory is a starting assertion, not permission to mutate. The implementation must refresh it immediately before the change window. Any changed count, extra secret/environment/branch, held lock, non-green deployment, state mismatch, unreviewed drift, or different repository/source identity stops before mutation and returns to PM.
Identifier disposition
| Identifier class | Required method | Terminal contract |
|---|---|---|
| Terraform source root | Git history-aware source move | One executable development/website root; no executable root remains at sandbox/website. A small command-free compatibility notice is allowed only if an inbound link requires it. |
| Terraform logical addresses | Retain exactly | module.website and every resource/data label, key, and index remain byte-identical. No moved block, terraform state mv, import/import block, state rm/push, or provider replacement is required or allowed. Pre/post sorted address inventories must match byte-for-byte at count 98. |
| Backend state and native lock keys | Guarded one-time backend migration in the same bucket/account/state lineage | New key development/website/terraform.tfstate; new lock development/website/terraform.tfstate.tflock; same lineage, serial, address set, provider bindings, and state bytes produced by the migration. Old state object/version is retained read-only as rollback/audit evidence, never used as a second environment or promotion source. |
| Shared state bucket | Permanent retention | datamailer-sandbox-817685572750-us-east-1-tfstate remains the bootstrap-owned encrypted/versioned/public-blocked bucket. The website stack does not rename, import, create, or manage it. |
| GitHub deploy environment | Create/copy/verify/cut over/delete | New development, exactly main-only, exact 18 non-secret DEVELOPMENT_* variables, zero secrets; old sandbox is deleted only after new exact-SHA delivery and old-subject denial pass. |
| Deployer OIDC trust | In-place trust-policy update on the existing role | Exact audience; first exact old+new immutable subjects, then exact new subject only. No wildcard, StringLike, mutable owner/repository name, branch/ref substitute, extra principal, or session extension. Publisher trust/policy is unchanged. |
| Physical application roles | Permanent retention | Existing role ARNs/names and inline least-privilege policies remain; only the deployer assume-role subject set changes in place. No role replacement or attached policy. |
website-sandbox*, secret/log/KMS/RDS/ECR/ECS/network physical identifiers |
Permanent retention | Exact existing resources, values, history, secret versions, endpoints, and state addresses remain. They stay centralized in the compatibility registry and current prose labels them legacy physical names. |
Environment=sandbox tag and DEVELOPMENT_RESOURCE_ENVIRONMENT_TAG value |
Permanent retention | Retain because deployer registration policy, existing resources, task-definition history, and release-controller validation are bound to it. Do not retag the graph or weaken tag conditions in this issue. |
| Frozen bootstrap/Gate-B/audit/release evidence | Immutable retention | Do not rewrite, regenerate, re-hash, or reinterpret pinned payloads, timestamped audits, old runs/artifacts, source bindings, ARNs, or schema readers. Add a successor current-state contract instead of editing historical bytes. |
| Current Terraform/workflow/runbook prose, path filters, outputs, tests, and compatibility registry | Change in place | Development is canonical; retained physical values are confined to the named compatibility boundary with an exact reason and #94. |
Scope
- In
DataTalksClub/aws-infra, move the canonical root, update #78's exact path/backend policy and tests, add a two-subject transition input with a terminal one-subject configuration, and update current documentation/policy fixtures without changing logical addresses or physical resource inputs. - In
DataTalksClub/website, change the four GitHub environment bindings todevelopment, update the current compatibility boundary/constants/tests/runbook, and preserve all #93 terminology and frozen-evidence checks. - Add a sealed, code-owned migration verifier that exposes only stable identifiers, counts, hashes, lineage/serial equality, plan action summaries, role/trust facts, credential expiry/reserve, and
PASS/STOP. State bodies, provider responses, policy bodies, variable values, credentials, secret names beyond the accepted public inventory, and raw errors remain private below repository-local.tmp/with0700directories and0600files. - Use the accepted #78 plan/apply roles and protected, main-only, non-cancelling, serialized environment. Application publisher/deployer roles never receive Terraform, backend, IAM-administration, GitHub-environment, or state-migration permission.
- Preserve automatic application delivery, exact migration-first web/worker ordering, #96 receipt/DRAINING rules, #102 bounded compensation, #98 SQLite ordinary CI, public TLS/DNS/noindex, and the current active release throughout.
Exact migration order
Every numbered hold point produces a redacted signed/hash-bound result. A failure stops; it never falls through to the next phase.
0. Dependency, freeze, and preflight
- #78 must be accepted and its protected plan/apply path terminal green. Its migration mode must allow only the two exact state objects and two exact
.tflockobjects during this issue; no prefix wildcard, DynamoDB lock, long-lived key, application role, production resource, or unrelated state is permitted. - Freeze website and infrastructure
mainchanges for the bounded window. Set repositoryDEVELOPMENT_AUTO_DEPLOY=falseand read it back without printing other variable values. This changes no serving release. - From clean exact-main checkouts, refresh: caller/account/region; #78 role identity and expiry; backend bucket controls; old state metadata/version and absent old/new locks; sorted 98-address inventory and its hash; state lineage/serial hash; refreshed no-change plan; exact DNS records; CloudFront/ALB/RDS/ECR/ECS/secrets/KMS/IAM metadata; GitHub environments/branch policies/variable names/secret count; workflow bindings; current release SHA/digest/task definitions/counts; public liveness/readiness/TLS/noindex.
- Each short-lived infrastructure session must begin with at least 1,200 seconds remaining, has a code-owned 900-second phase cap and 300-second reserve, is resolved once, is never refreshed/retried, and is unavailable to GitHub/API subprocesses. An expiry parse error, smaller reserve, provider timeout, held lock, plan drift, or identity mismatch is
STOP.
1. Land the source/address transition against the old backend
- Move the root to
development/websitewhile keeping the old backend key for this first transition revision. Update #78 path filters and current docs/tests. Keep the module call/address graph and every physical input exact. - Extend only
module.website.aws_iam_role.github_deployerto support an ordered set of one or two literal deployment-environment subjects. Validation permits exactly{sandbox, development}during migration and exactly{development}terminally; the production fixture remains exactly its own one subject. - The refreshed plan against the old backend may show only one intended in-place deployer trust-policy update when dual trust is selected:
0 add,1 change,0 destroy, no replacement. The source-root move alone produces no state action. Any other resource, output-derived physical change, tag change, address change, import, DNS action, service count/task-definition action, or unknown value isSTOP.
2. Create the new GitHub environment and establish dual trust
- Create repository environment
development, configure custom branch policymainonly, and copy the exact 18 existing environment-scopedDEVELOPMENT_*variables byte-for-byte through a non-logging path. Repository-scoped sevenDEVELOPMENT_*variables remain at repository scope. Do not create or move a secret; the refreshed old/new secret counts must both be zero. - Apply the reviewed one-resource in-place dual-trust plan through #78. The existing role ID/ARN, max session, principal, audience, tags, inline permission policy, empty attached-policy set, and publisher role/trust remain byte-equivalent.
- Prove both exact old and new deployer subjects can assume only the deployer role; main-ref-to-deployer, either environment-to-publisher, wrong audience, wrong owner/repository IDs,
dev, production, tag, PR, feature branch, fork, wildcard, and case variants fail. Probes are read-only and invoke no ECS/ECR/state/DNS/secret mutation.
3. Migrate the backend key once
- Land the backend transition revision that changes only the partial backend key to
development/website/terraform.tfstate. Normal plan/apply is blocked until the migration verifier passes. - In one fresh private working directory, initialize the exact old backend, bind the old state metadata/version, lineage/serial, sorted address hash/count 98, provider lock, and zero-lock facts. Require the new state object not to exist.
- Run one reviewed non-interactive
terraform init -migrate-state -force-copytransition from the exact old S3 key to the exact new S3 key, with native locking,-input=false, locked providers, fixed timeout, #78 migration-role credentials, and no workspace/prefix override.-force-copyis authorized only for this pre-bound same-bucket old→new pair after destination-absence proof; it is not a general state-copy or promotion mechanism. - Do not run
aws s3 cp,terraform state pull/push/mv/rm/replace-provider, import/import blocks, a broad state command,-lock=false,-reconfigure, or a second migration attempt. - Before any apply, prove the new object has the expected encryption/versioning/owner; old and new lineage/serial/provider/address facts are equal; the address inventory is byte-identical at 98; both lock objects are absent; and a locked, refreshed plan from the new backend is no changes. A local emergency state file, partial object, different serial/lineage/hash, destination pre-existence, stale old state, or ambiguous Terraform result is
STOPand incident review. - If migration fails before a verified new object, continue only from the old backend. If a new object exists but equality is unproven, quarantine it and keep the old backend authoritative; do not delete or overwrite either. After the first later Terraform apply through the new backend, rollback never points Terraform back to the now-stale old key.
4. Cut the application workflow to development
- In one reviewed website candidate, change exactly the four environment bindings from
sandboxtodevelopment; update current compatibility code/docs/tests while leaving all physical AWS values and frozen evidence exact. Do not change deployment timing, receipts, compensation, migrations, SQLite CI, routes, templates, or HTML. - Re-read new environment branch policy/variables/zero secrets and dual trust. Set
DEVELOPMENT_AUTO_DEPLOY=true, then merge/push the accepted website change once through the normal lifecycle. That push—not a manual fallback—must deploy its exact full SHA throughdevelopment. - On-call alone observes all required jobs, OIDC identity, migration, exact receipt-bound web then singleton-worker rollout, read-only smoke, artifact finalization, terminal pair, public SHA/readiness, and desktop/mobile smoke. The prior serving release remains authoritative on any failure. No HTML is changed or minified.
5. Remove the old trust and environment
- Only after the exact-SHA automatic deployment is green, land the terminal infra revision with the single new deployer subject. From the new backend, the refreshed plan must again be exactly one in-place deployer trust update and no other action; apply it through #78.
- Prove the new subject still succeeds, the old
environment:sandboxsubject now fails, all negative claims fail, application rollback through the new environment remains authorized, and role ID/ARN/permission policy are unchanged. - Revoke #78 migration-role write/delete permission to the old state and old lock keys, retaining only explicitly approved metadata/read access for the old version if required. The new state/lock paths become the sole writable backend. A refreshed terminal plan must be no changes.
- Confirm no queued/running workflow references
sandbox, then delete the old GitHub environment. Re-read thatdevelopmentis the sole deployment environment withmainonly, 18 variables, zero secrets, and four current workflow bindings. Current source/configuration must not read the old environment or old backend key.
6. Terminal evidence and rollback record
Record exact source SHAs for both repositories, new state metadata/version and address hash/count, old read-only version, terminal trust hash, environment/branch/variable-name/secret-count inventory, no-change plan, role-policy and negative-test hashes, CI run, deployed SHA/digest/task definitions, terminal service pair, public health/TLS/DNS/noindex, and inspected screenshots. Evidence is redacted and contains no state body, secret/variable value, credentials, provider payload, cookies, registration data, or production data.
Rollback is phase-specific:
- before the backend migration, restore source/configuration and use the unchanged old backend;
- after a verified copy but before any new-backend apply, the old key may be reselected only if lineage/serial/address equality is reproved and PM explicitly approves;
- after any new-backend apply, the new key is authoritative; rollback re-adds the exact old OIDC subject through #78 if needed and may switch the website workflow back, but never copies or points state backward;
- a failed application cutover keeps the prior release serving, disables automatic deployment, retains dual trust and both GitHub environments, and returns through the normal issue lifecycle; and
- no rollback deletes resources, state versions, images, secrets, logs, database data, registrations, or release evidence.
Non-goals
- No physical AWS rename, replacement, destroy/recreate, import, retagging, data copy, secret rotation/value read, image-history copy, task-family migration, RDS endpoint change, log migration, or KMS-key/alias change.
- No change to
web.dtcdev.click,origin.web.dtcdev.click, hosted zoneZ05963572WVWFHDQZH5NE, DNS records, TLS, edge/origin controls, VPC/subnets/security groups, RDS configuration/data, ECR images, ECS desired counts/task definitions, email, content, routes, SEO semantics, or product features. - No rewrite of #93, #96, #98, #102 behavior; no deployment-controller timeout/receipt/recovery change; no PostgreSQL requirement in ordinary CI; no template or HTML change/minification.
- No rewrite of immutable audits, Gate-B seed/execution/manifest, prior issue comments, workflow runs/artifacts, release records, source hashes, state versions, Git history, or compatibility captures.
- No production account/backend/environment/trust/DNS/data/deployment work and no copying development state to production.
Acceptance criteria
- The refreshed preflight matches the authoritative inventory, exact source identities, clean worktrees, 98 addresses, absent native locks, current green release/public health, GitHub configuration, and credential-expiry contract; every drift/failure stops before mutation.
-
DataTalksClub/aws-infrahas one canonical executabledevelopment/websiteroot. Current path filters, docs, outputs, tests, and examples use development terminology; retained physical values are exact and registered. The production fixture remains separately configurable and consumes no development state/resource. - Logical Terraform addresses and provider bindings match byte-for-byte before/after. There are no moved/import blocks or state/import/move/push/rm/provider-replacement commands, and no AWS resource is added, destroyed, replaced, renamed, retagged, or imported.
- The guarded backend migration creates only
development/website/terraform.tfstateand its transient native lock in the existing bucket, preserves lineage/serial and the exact 98-address inventory, leaves both locks absent, retains the old version read-only, and ends with the new key as the sole writable backend plus a refreshed no-change plan. - GitHub environment
developmentis the sole terminal application deployment environment, allows exactly branchmain, contains exactly the 18 expected environment-scopedDEVELOPMENT_*variables with unchanged values/scopes and zero secrets, while the seven repository variables remain repository-scoped.DEVELOPMENT_AUTO_DEPLOYis restored totrue. - The existing deployer role is never replaced. Transition trust contains exactly old+new immutable subjects and terminal trust exactly the new subject, always with audience
sts.amazonaws.com; publisher trust/policy and both roles' permission boundaries, IDs/ARNs, inline policies, session limits, and empty attached-policy sets remain unchanged. - Positive old/new transition and terminal-new OIDC probes pass; old terminal, main-to-deployer, environment-to-publisher, wrong audience/IDs/environment/ref/event/fork/tag/branch/case and wildcard claims fail before AWS mutation. Application roles still cannot read state/secrets or mutate IAM/DNS/RDS/KMS/CloudFront/ALB/infrastructure.
- The website workflow has exactly four
environment.name: developmentbindings and no current old-environment/backend read. Its existingDEVELOPMENT_*interface, automatic push behavior, exact physical compatibility validation, #96/#102 controller semantics, and #98 SQLite ordinary CI are unchanged. - Every retained
sandboxoccurrence is machine-classified as frozen evidence, historical link, shared bootstrap identifier, or exact legacy physical compatibility value with reason and #94. Unclassified current terminology fails CI; frozen bytes/source hashes remain readable and unchanged. - Terraform fmt/validate/test/static policy, action/path/backend policy, trust and state-migration tests, website terminology/workflow/deployment tests, normal
uvquality/Django/SQLite gates, andmake test-playwright-corepass. No state/plan body, secret, credential, variable value, token, registration/production data, or raw provider error appears in source, logs, evidence, artifacts, or comments. - [HUMAN] The accepted website push automatically deploys its exact full SHA through environment
development; all required jobs, migration, receipt-bound web then singleton-worker rollout, #102 compensation readiness, smoke/finalization, terminal release pair, and rollback record are green with no manual fallback. - [HUMAN] Bounded observation before/during/after every mutation shows uninterrupted valid TLS/DNS and public liveness/readiness at
web.dtcdev.click, exact noindex/canonical behavior, no connection failure/5xx/redirect loop/mixed release/worker overlap, and no state lock or unintended AWS/GitHub drift. - [HUMAN] Desktop
1280x720and mobile390x844deployed smoke screenshots for/,/courses/,/studio/, and the intentional 404 are inspected and correct;/api/v1/admin/healthremains anonymous JSON401; live HTML remains readable and is not minified. - Production configuration, account, infrastructure, trust, state, DNS, data, and deployment are unchanged and absent from all actions/evidence.
Test scenarios
- Inventory/drift/credential gate: vary source SHA, account/role, expiry/reserve, state address/count/lineage/serial, lock, destination existence, GitHub env/branch/variable/secret counts, active release, or public health. Every mismatch stops before state, IAM, GitHub, or AWS mutation.
- Address/source move: compare old/new root graphs and sorted live state addresses; fail on a renamed module/resource/data label, key/index/provider, moved/import block, physical input, tag, or executable legacy root.
- Backend migration matrix: test exact old→new success plus destination-existing, held old/new lock, partial copy, different lineage/serial/address/provider, credential expiry, timeout, interrupt, second attempt, wrong bucket/key/account/workspace, local emergency state, and forbidden state/S3 commands. Only the exact one-time equal migration passes.
- Plan policy: transition plan permits only the exact in-place deployer trust update; backend and terminal plans are no-change except the exact terminal trust prune. Any add/delete/replacement/import/DNS/service/count/task/tag/secret/data action fails.
- GitHub configuration: exercise missing/extra/shadowed/mismatched variables, secret appearance, wrong branch policy,
dev/duplicate environment, queued old job, stale source, and current exact configuration. Values are compared without logging. - OIDC transition: verify exact old+new positive subjects, terminal new positive/old denial, publisher main positive, all cross-role and malformed claim denials, unchanged role identity/policy, and no provider call after a failed claim.
- Frozen/retained identifiers: byte/hash compare frozen Gate-B/audit artifacts; verify current code uses the successor contract while retained physical names are centralized and exact. A cosmetic physical rename or broad terminology allowlist fails.
- Automatic release and rollback: after engineer/tester/PM gates, observe the single accepted website push through new environment and verify exact SHA/digest, migration, web/worker receipts, singleton, smoke, terminal pair, public routes, and rollback eligibility. Fault cases leave the prior release serving and never point state backward.
Dependencies and issue boundaries
- Blocked by #78. #78 must first provide the accepted least-privilege, protected, serialized Terraform plan/apply and exact two-key migration permission boundary. No #94 Terraform plan/apply, backend migration, IAM/OIDC mutation, or AWS mutation starts while #78 is open.
- #93 is satisfied baseline, not work to repeat. It already migrated repository/application terminology and
DEVELOPMENT_*variables while intentionally retaining the live root/environment/OIDC boundary for this issue. - #96 is a satisfied controller baseline. Its receipt-bound ACTIVE/DRAINING rollout contract is unchanged.
- #98 is not a dependency. Its accepted/deployed SQLite local/ordinary-CI behavior remains unchanged; its separate human PostgreSQL-object inventory does not block this infrastructure-name migration.
- #102 is not a dependency. Its accepted/deployed positive compensation controller remains unchanged; its deliberate recovery drill is separate and is neither performed nor claimed here.
- #101 and any later controller hardening are separate. #94 may not absorb or weaken them.
- The live #67–#70 infrastructure, current state, and subsequent exact-SHA deployment evidence are the migration source of truth. Historical immutable evidence is not rewritten.
The issue is implementation-ready but operationally blocked. Remove needs grooming; engineering selection waits until #78 is closed and the refreshed preflight passes.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with specs 08–10 and the development legacy-identifier boundary, then inspect aws-infra/sandbox/website, its backend configuration, and the website workflow's four environment bindings. Verify the refreshed inventory and #78 migration path before making changes. Done means development is canonical, logical addresses and physical resources are unchanged, the state and OIDC transitions pass their hold points, and the old environment is removed only after the required delivery and denial checks.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, github-actions, terraform
- Domain
- ci-cd, cloud, devops, infrastructure, security
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100