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
internalprofile:aws sso login --profile internal. terraform≥ 1.6 on your PATH.- Clone access to
ShyftSolutions/shyft-infra. - Account
973309142234, regionus-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:
| Resource | Name | Notes |
|---|---|---|
| S3 bucket | shyft-tf-state-973309142234 | Versioned, SSE-S3, all public access blocked, prevent_destroy |
| DynamoDB table | shyft-tf-locks | PAY_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 internalIf 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.comIf 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 applyGrab 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.ymlModule catalog
| Module | Creates | Phase |
|---|---|---|
bootstrap | S3 state bucket + DynamoDB lock table | P0 ✅ |
github-oidc-provider | The one account-wide GitHub OIDC provider (import-aware) | P1 ✅ |
gha-oidc-role | Per-repo IAM role CI assumes; min-rights trust scoped to main + v* tags; push policy scoped to the named ECR repo | P1 ✅ |
ecr-repo | An ECR repository, scan-on-push, lifecycle policy (expire untagged + cap count) | P1 ✅ |
ec2-app-host | EC2 + EIP + SG + instance profile + cloud-init | P2 — TODO |
monitoring-host | Dedicated Prometheus/Grafana/Alertmanager VM | P4 — 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.