---ci--- project: nova-platform milestone: v1.0 status: complete requirements: covered: [REQ-01,REQ-02,REQ-03,REQ-04,REQ-05,REQ-06,REQ-07,REQ-08,REQ-09,REQ-10,REQ-11,REQ-12,REQ-13,REQ-14,REQ-15,REQ-16,REQ-17,REQ-18,REQ-19,REQ-20,REQ-21,REQ-22,REQ-23,REQ-24,REQ-25,REQ-26,REQ-27,REQ-28,REQ-29,REQ-30,REQ-31,REQ-32,REQ-33,REQ-34,REQ-35,REQ-36,REQ-37,REQ-38] partial: [] ---/ci--- v1.0 milestone complete: simplified infrastructure-delivery platform derived from Nova (acdl). 6 phases (P0-P5 + P6 final). 38 REQ-IDs. 38 decisions (D-001..D-038). 76 tests pass. Engine boundary holds. Happy paths green (check-only + CI). 13 L1 + 2 L2 modules. 5 terraform roots. Shell reproducibility. Zero OOS files. Tags: v0.1.0 (P0) → v0.1.1..v0.1.5 (P1..P5) → v0.1.6 (P6 = milestone release on v0.1 patch line).
Nova Bootstrap Runbook
Phase 4 bootstraps the AWS state backend + the spike runner IAM user for the nova-platform. Two scripts create the infrastructure exactly once; after that, the rotated spike-runner key is used for all platform + CI operations.
Spike scope (D-025): onboarding uses a cross-account IAM role (not OIDC). The consumer's CI runner assumes the deploy role via
sts assume-roleusing the platform runner user's static credentials. Real OIDC federation is the production path, OOS for nova v1.0.
State backend (D-022)
create_state_backend.py creates (idempotent):
- S3 bucket
nova-tfstate-<account>-<region>(versioned) — holds all Terraform state files (platform/terraform.tfstate,spike/ci-vpc/terraform.tfstate,spike/microservice/<env>/terraform.tfstate). - DynamoDB table
nova-tfstate-locks— the dedicated Terraform state lock table (NOTnova-outbox— the outbox is OOS for nova v1.0). The S3 backendlock_tableattribute points to this table.
The account + region are resolved from the caller's live credentials
(sts:GetCallerIdentity) — NO hardcoded account ID. A marker file
terraform/bootstrap/.bootstrap_state.json records the created bucket +
table names (gitignored).
IAM runner (D-026)
create_iam_user.py creates:
- IAM user
nova-spike-runner. - Inline/managed policy
nova-spike-runner-policyfromterraform/bootstrap/spike_runner_policy.json. The JSON uses${account_id}and${region}placeholders (NOT hardcoded — D-026);create_iam_user.pysubstitutes the live account ID + region before attaching the policy. - Initial access key (printed to stdout; capture or rotate via
rotate_spike_key.sh).
Policy scope (D-026)
The spike_runner_policy.json grants the runner the Terraform-deployable
permissions it needs to apply the platform + L2 module stacks:
| Service | Granted | Notes |
|---|---|---|
| S3 | ✅ | State bucket nova-tfstate-<account>-<region> |
| DynamoDB | ✅ | Lock table nova-tfstate-locks (D-022) |
| ECS | ✅ | Clusters + services + task definitions |
| ECR | ✅ | Repositories + images |
| ELB | ✅ | ALBs + target groups + listeners |
| IAM | ✅ | Roles + policies (Terraform-managed) |
| EC2 | ✅ | VPCs + subnets + SGs + route tables |
| CloudFront | ✅ | Distributions |
| WAF | ✅ | Web ACLs |
| KMS | ✅ | Customer-managed keys + aliases |
| Lambda | ❌ DROP | Platform Lambda OOS (D-023) |
| Secrets Mgr | ❌ DROP | OOS |
| SNS | ❌ DROP | OOS |
| CostExplorer | ❌ DROP | OOS |
| OIDC provider | ❌ DROP | Onboarding uses assume-role (D-025) |
NOVA_BOOTSTRAP_AWS_* fallback
The bootstrap scripts accept the root-credential pair via the
NOVA_BOOTSTRAP_AWS_* env vars (never committed, never echoed):
export NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>"
export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>"
export AWS_DEFAULT_REGION="us-east-1"
These are the bootstrap-only credentials (used exactly once to create the state backend + spike runner). The fallback precedence is:
NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID/NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY(bootstrap root key — highest priority).- Standard
AWS_*env vars /~/.aws/credentialsprofile (for re-running scripts later with the rotated runner key).
Steps
-
Set the bootstrap root key in env (never commit, never echo):
export NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID="<root key>" export NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY="<root secret>" export AWS_DEFAULT_REGION="us-east-1" -
Create the state backend (S3 bucket + DynamoDB lock table):
python3 terraform/bootstrap/create_state_backend.pyIdempotent; writes
terraform/bootstrap/.bootstrap_state.jsonmarker. -
Create the IAM user + scoped policy + initial key:
python3 terraform/bootstrap/create_iam_user.pyPrints
NOVA_AWS_ACCESS_KEY_ID=<...>+NOVA_AWS_SECRET_ACCESS_KEY=<...>to stdout (capture if you want the initial key; rotate it before use). -
Rotate the spike key (creates a new key, deactivates+deletes old, writes the new key to gitignored
.env.secrets):bash scripts/rotate_spike_key.sh -
Verify (manual): confirm the caller identity is
nova-spike-runner(not root); the S3 bucket + DynamoDB table + IAM user + scoped policy all exist;.env.secrets+.bootstrap_state.jsonare gitignored. -
MANUAL: rotate/deactivate the root key in the AWS IAM console (the user does this, not the script). The bootstrap root key has now served its one-shot purpose; the spike uses the rotated
nova-spike-runnerkey for all subsequent operations.
Onboarding (D-025)
Consumer onboarding is handled by terraform/onboarding/main.tf, which
creates a per-consumer IAM role (not a user) with a trust policy
allowing the platform runner user to assume it via sts:AssumeRole
(cross-account assume-role pattern). NO OIDC. See the onboarding root
for variable documentation (consumer_repo, owner_id, account_id,
region, runner_user_arn).