DataTalksClub / DataTalksClub/website

Migrate legacy development Terraform, state, and OIDC identifiers safely

Open
#94 5 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

data-migration documentation enhancement human infra integration operations P0 security testing
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/websitedevelopment/website;
  • S3 backend key: sandbox/website/terraform.tfstatedevelopment/website/terraform.tfstate, with native lock changing to the corresponding .tflock key;
  • GitHub application deployment environment: sandboxdevelopment; and
  • deployer OIDC subject: repo:DataTalksClub@72699292/website@1326548167:environment:sandboxrepo:DataTalksClub@72699292/website@1326548167:environment:development through 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/main are 3f6c227b51bee8eabb09a89955603193574d7090; automatic run 31298390668 is terminal green and /health/live reports that exact SHA; /health/ready and / return 200 with valid TLS and noindex, nofollow;
  • DataTalksClub/aws-infra main is 95d93f7e07ded19e482a0c6d6471fbd93fb608d8; the executable root is sandbox/website, calls module "website", and uses Terraform >=1.10 with S3 use_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, key sandbox/website/terraform.tfstate; the accepted native lock is absent;
  • the website repository has exactly seven repository-scoped DEVELOPMENT_* variables, including DEVELOPMENT_AUTO_DEPLOY=true, and the sandbox environment has exactly 18 environment-scoped DEVELOPMENT_* variables and zero environment secrets;
  • the only website deployment environment is sandbox; it has exactly one custom deployment branch policy, main; development and dev do not exist; and
  • the current workflow contains exactly four literal environment.name: sandbox bindings. The publisher continues to use the exact immutable main-ref subject. The physical publisher/deployer roles remain website-sandbox-github-publisher and website-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 to development, 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/ with 0700 directories and 0600 files.
  • 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
  1. #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 .tflock objects during this issue; no prefix wildcard, DynamoDB lock, long-lived key, application role, production resource, or unrelated state is permitted.
  2. Freeze website and infrastructure main changes for the bounded window. Set repository DEVELOPMENT_AUTO_DEPLOY=false and read it back without printing other variable values. This changes no serving release.
  3. 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.
  4. 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
  1. Move the root to development/website while 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.
  2. Extend only module.website.aws_iam_role.github_deployer to 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.
  3. 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 is STOP.
2. Create the new GitHub environment and establish dual trust
  1. Create repository environment development, configure custom branch policy main only, and copy the exact 18 existing environment-scoped DEVELOPMENT_* variables byte-for-byte through a non-logging path. Repository-scoped seven DEVELOPMENT_* variables remain at repository scope. Do not create or move a secret; the refreshed old/new secret counts must both be zero.
  2. 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.
  3. 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
  1. 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.
  2. 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.
  3. Run one reviewed non-interactive terraform init -migrate-state -force-copy transition 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-copy is 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.
  4. 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.
  5. 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 STOP and incident review.
  6. 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
  1. In one reviewed website candidate, change exactly the four environment bindings from sandbox to development; 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.
  2. 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 through development.
  3. 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
  1. 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.
  2. Prove the new subject still succeeds, the old environment:sandbox subject now fails, all negative claims fail, application rollback through the new environment remains authorized, and role ID/ARN/permission policy are unchanged.
  3. 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.
  4. Confirm no queued/running workflow references sandbox, then delete the old GitHub environment. Re-read that development is the sole deployment environment with main only, 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 zone Z05963572WVWFHDQZH5NE, 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-infra has one canonical executable development/website root. 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.tfstate and 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 development is the sole terminal application deployment environment, allows exactly branch main, contains exactly the 18 expected environment-scoped DEVELOPMENT_* variables with unchanged values/scopes and zero secrets, while the seven repository variables remain repository-scoped. DEVELOPMENT_AUTO_DEPLOY is restored to true.
  • 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: development bindings and no current old-environment/backend read. Its existing DEVELOPMENT_* interface, automatic push behavior, exact physical compatibility validation, #96/#102 controller semantics, and #98 SQLite ordinary CI are unchanged.
  • Every retained sandbox occurrence 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 uv quality/Django/SQLite gates, and make test-playwright-core pass. 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 1280x720 and mobile 390x844 deployed smoke screenshots for /, /courses/, /studio/, and the intentional 404 are inspected and correct; /api/v1/admin/health remains anonymous JSON 401; 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.