Shyft SolutionsDev guideTerraform bring-up
GitHub
Draft — being built (deploy-pipeline P0/P1)

This page documents the terraform that now exists in shyft-infra, but the steps below have not yet been run end-to-end against the AWS account. Follow it, and report anything that doesn't match — that's how we harden it. Status in .claude/features/deploy-pipeline/tasks.md § P0 + P1.

The CI image build (CI image build) can't push anywhere until two things exist in AWS: an ECR repository to push to, and an IAM role that GitHub Actions can assume via OIDC. Both are terraform, and both depend on a state backend. This page walks the one-time bring-up, bottom to top.

Everything lives in shyft-infra under terraform/.

Prerequisites

  • AWS SSO configured with an admin-capable internal profile: aws sso login --profile internal.
  • terraform ≥ 1.6 on your PATH.
  • Clone access to ShyftSolutions/shyft-infra.
  • Account 973309142234, region us-east-2 (both are baked into the terraform as defaults — override only if they move).

1. Bootstrap the state backend (once per account)

The bootstrap rootset creates the S3 bucket and DynamoDB table that every other rootset uses for remote state. It can't use the remote backend (it's creating it), so it runs on local state, and that state file is committed to the repo on purpose.

cd shyft-infra/terraform/bootstrap
terraform init            # local backend — no S3 yet
terraform apply           # review: 1 S3 bucket (+versioning/SSE/PAB), 1 DynamoDB table
git add terraform.tfstate terraform.tfstate.backup
git commit -m "bootstrap: terraform state backend (S3 + DynamoDB)"

This creates:

ResourceNameNotes
S3 bucketshyft-tf-state-973309142234Versioned, SSE-S3, all public access blocked, prevent_destroy
DynamoDB tableshyft-tf-locksPAY_PER_REQUEST, hash key LockID

You should never need to run this rootset again unless the backend itself moves.

2. Check for an existing GitHub OIDC provider

AWS allows exactly one OIDC provider per issuer URL per account. If one already exists for token.actions.githubusercontent.com, terraform must import it rather than create a duplicate (apply will error otherwise).

aws iam list-open-id-connect-providers --profile internal

If the list includes a token.actions.githubusercontent.com ARN, import it after step 3's init and before its apply:

terraform import module.github_oidc_provider.aws_iam_openid_connect_provider.github \
  arn:aws:iam::973309142234:oidc-provider/token.actions.githubusercontent.com

If the list is empty, do nothing — step 3 creates it.

3. Provision the live/prod resources

The live/prod rootset wires the reusable modules into real resources. For the portal pilot that's: the account-wide OIDC provider, the internal/portal ECR repo, and the per-repo CI role scoped to it.

cd shyft-infra/terraform/live/prod
terraform init            # S3 backend — fails if step 1 hasn't run
# (run the step-2 import here if needed)
terraform apply

Grab the outputs — you'll need the role ARN to wire up CI:

terraform output portal_gha_role_arn   # → set as the AWS_GHA_ROLE_ARN repo variable
terraform output portal_ecr_url        # → already a constant in build-image.yml

Module catalog

ModuleCreatesPhase
bootstrapS3 state bucket + DynamoDB lock tableP0 ✅
github-oidc-providerThe one account-wide GitHub OIDC provider (import-aware)P1 ✅
gha-oidc-rolePer-repo IAM role CI assumes; min-rights trust scoped to main + v* tags; push policy scoped to the named ECR repoP1 ✅
ecr-repoAn ECR repository, scan-on-push, lifecycle policy (expire untagged + cap count)P1 ✅
ec2-app-hostEC2 + EIP + SG + instance profile + cloud-initP2 — TODO
monitoring-hostDedicated Prometheus/Grafana/Alertmanager VMP4 — TODO

Onboarding a second app

Once ec2-app-host lands (P2), adding an app is two module blocks in live/prod/main.tf — an ecr-repo and a gha-oidc-role pointed at the new repo — reusing the same OIDC provider. No new bootstrap.

Next

With the role ARN in hand, wire it into the app's CI: CI image build.