Files
nova-platform/terraform/bootstrap/README.md
T
CIAgent 2443909362 docs(milestone): complete v1.0-nova-platform (release v0.1.6)
---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).
2026-08-25 02:18:35 +00:00

5.2 KiB

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-role using 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 (NOT nova-outbox — the outbox is OOS for nova v1.0). The S3 backend lock_table attribute 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-policy from terraform/bootstrap/spike_runner_policy.json. The JSON uses ${account_id} and ${region} placeholders (NOT hardcoded — D-026); create_iam_user.py substitutes 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:

  1. NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID / NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (bootstrap root key — highest priority).
  2. Standard AWS_* env vars / ~/.aws/credentials profile (for re-running scripts later with the rotated runner key).

Steps

  1. 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"
    
  2. Create the state backend (S3 bucket + DynamoDB lock table):

    python3 terraform/bootstrap/create_state_backend.py
    

    Idempotent; writes terraform/bootstrap/.bootstrap_state.json marker.

  3. Create the IAM user + scoped policy + initial key:

    python3 terraform/bootstrap/create_iam_user.py
    

    Prints NOVA_AWS_ACCESS_KEY_ID=<...> + NOVA_AWS_SECRET_ACCESS_KEY=<...> to stdout (capture if you want the initial key; rotate it before use).

  4. 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
    
  5. 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.json are gitignored.

  6. 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-runner key 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).