This page owns the optional bootstrap of the AWS IAM OIDC provider and the
GitHub Actions role used by .github/workflows/terraform-apply.yml, plus
the GitHub variable, environment, and CloudTrail checks that follow a first
local apply. Read it when the repository owner and AWS account owner are
performing that bootstrap. It does not inventory other operations
procedures — those live in README.md — and it does not report
measured engineering status, which lives in STATUS.md.
Audience: repository owner / AWS account owner performing the optional OIDC bootstrap
Prerequisites: AWS administrator credentials for the initial bootstrap only, the existing S3 backend bucket agentflow-terraform-state and DynamoDB lock table agentflow-terraform-locks, GitHub repository admin access, and Terraform CLI 1.15.4 or an equivalent container image of that version; see Prerequisites
This archived optional guide bootstraps the AWS IAM OIDC provider and the GitHub Actions role used by .github/workflows/terraform-apply.yml if AWS is ever explicitly reintroduced.
The workflow's plan job writes its binary plan to ignored .artifacts/terraform/tfplan and uploads it as terraform-plan-<environment>; the apply job downloads the same path. That file is a replaceable per-run working copy that embeds resolved variable values: never write it next to the configuration or commit it, and do not treat it as reviewed evidence, OIDC/apply evidence, or production acceptance.
Current project decision as of 2026-05-30: a managed-AWS / Terraform-apply production deployment is out of scope for this pre-production portfolio project — a deliberate non-goal, with the stack validated end-to-end on a local/kind demo instead. Do not treat missing AWS apply evidence as a project deficiency, active blocker, or autonomous follow-up. Reopen this path only if the operator explicitly provides an AWS account and approval to reintroduce it.
For the DV2 demo, use the already documented S3-compatible cold-tier path with HF Datasets or Backblaze B2 for derived/anonymized parquet. Do not require AWS/S3 for that dataset.
Status: not applicable for the current plan — a managed-AWS production deployment is a deliberate non-goal for this pre-production portfolio project. The older AWS evidence below is retained only as historical context.
Confirmed local/repository evidence:
- Repository variable
AWS_REGIONexists and is set tous-east-1. - Repository variable
AWS_TERRAFORM_ROLE_ARNis not configured. .github/workflows/terraform-apply.ymlremains disabled withif: false.- Real
infrastructure/terraform/environments/staging.tfvarsandprod.tfvarsfiles are absent. - No AWS credentials are configured on the verification workstation.
- The
hashicorp/terraform:1.13.5container evidence for config sanity (init -backend=falseandvalidate) is superseded by therequired_version = "~> 1.15.4"pin and can no longer be reproduced:terraform initon 1.13.5 evaluates the constraint and fails withUnsupported Terraform Core version. Config sanity was re-verified on 2026-09-05 with a local Terraform CLI 1.15.4 runningterraform init -backend=falseandterraform validatefrominfrastructure/terraform(both succeeded;validatereportedSuccess! The configuration is valid.). This is still not evidence of a real apply, and a container-image run athashicorp/terraform:1.15.4has not been performed. .github/workflows/terraform-apply.ymlincludes a manualPREFLIGHTpath that validates required variables, real tfvars presence, andterraform init -backend=false/terraform validatewithout runningapply.
Access triage on 2026-05-04 confirmed the blocker is still external: GitHub CLI
is authenticated for repository inspection, but AWS CLI and Terraform CLI are
not available in PATH; gh variable list still reports only AWS_REGION;
the workflow still has both Terraform jobs guarded with if: false; and only
example tfvars files exist locally. No AWS account bootstrap, role ARN, real
tfvars, CloudTrail OIDC proof, first apply run, reviewer, or rollback owner was
available to record.
Evidence recheck on 2026-05-06 confirmed the same blocker on the pushed main
HEAD ca5ba1d44c35bc27bc561b64f5e0c5c706415756: repository variables contain
AWS_REGION=us-east-1 only; AWS_TERRAFORM_ROLE_ARN is absent; GitHub
environments staging and production have required reviewers but no
environment-level variables or secrets; this workstation has no AWS credential
environment hints, AWS config, or AWS credentials file; aws, terraform, and
tofu are not installed in PATH; real
infrastructure/terraform/environments/staging.tfvars and
infrastructure/terraform/environments/prod.tfvars remain absent; and the
GitHub Actions API reports total_count: 0 for runs of the Terraform workflow
(terraform-apply.yml, now named Terraform (streaming-infrastructure-reference)),
so there is no apply/preflight run or CloudTrail evidence to cite.
Historical evidence recheck on 2026-05-30 kept the blocker external. Repository variables
still contain only AWS_REGION=us-east-1; AWS_TERRAFORM_ROLE_ARN is absent;
terraform is now available in PATH, but AWS CLI and AWS credential
environment hints are absent; workflow-expected
infrastructure/terraform/environments/*.tfvars files remain absent; both
Terraform plan/apply jobs remain guarded with if: false; and gh run list --workflow terraform-apply.yml reports no workflow runs. Local Terraform CLI
availability is not AWS role, tfvars, CloudTrail, approval, or apply evidence.
Under the current out-of-scope decision this is expected and should not be
rechecked without an explicit decision to reintroduce AWS.
The tracked root-level infrastructure/terraform/dev.tfvars is not proof of
H4 readiness: it is the local sandbox scaffold with placeholder-shaped VPC,
subnet, and SNS values. The root prod.tfvars was removed (audit P2-4) —
production inputs exist only as the operator-provided
environments/prod.tfvars (never committed; template in
environments/prod.tfvars.example), which is what both the workflow and
make deploy-prod resolve.
Local readiness update on 2026-05-06 added a no-apply preflight. It improves
evidence intake but does not close H4 because no AWS role ARN, real tfvars,
CloudTrail AssumeRoleWithWebIdentity proof, owner approval, or successful
apply evidence was supplied.
If AWS is explicitly reintroduced later, the operator packet to unblock review is:
- Secure ticket or evidence folder with AWS account owner and bootstrap operator.
- Non-secret
AWS_TERRAFORM_ROLE_ARNvalue and repo-variable proof. - Secure staging/prod tfvars ownership record; do not commit tfvars.
- Explicit approval to remove the workflow-level
if: falseguard. - First apply environment, reviewer, rollback owner, run URL or transcript, and
redacted CloudTrail
AssumeRoleWithWebIdentityproof.
Do not enable the workflow or run a real Terraform apply unless the operator first reopens AWS with an account and approval and then provides all external inputs:
- AWS account owner and bootstrap operator.
- Approved IAM role creation path for GitHub Actions OIDC.
- Resulting
AWS_TERRAFORM_ROLE_ARN. - Real staging and production tfvars supplied through the approved secure process.
- Explicit approval to remove the workflow-level
if: falseguard. - First apply environment, reviewer, rollback owner, and evidence location.
If any item above is missing, keep the release readiness state blocked and hand the missing input list back to the operator.
- AWS account with administrator credentials available for the initial bootstrap only.
- Existing S3 backend bucket
agentflow-terraform-stateand DynamoDB lock tableagentflow-terraform-locks. - GitHub repository admin access for repository variables and environment protection rules.
- Terraform CLI 1.15.4 (the floor of
required_versionininfrastructure/terraform/main.tfand the exact version thehashicorp/setup-terraformpins in.github/workflows/terraform-apply.ymland.github/workflows/ci.ymlinstall), or an equivalent container image of that version, available on the bootstrap machine.~> 1.15.4also admits later 1.15 patch releases; 1.16 and newer are refused.
The tracked S3 backend keeps dynamodb_table = "agentflow-terraform-locks"
on purpose, together with the TerraformStateLockTableDescribe /
TerraformStateLockItems statements and the dynamodb:LeadingKeys
condition in infrastructure/terraform/modules/github-oidc/main.tf. The
parameter is deprecated as of Terraform 1.15.4 and terraform init emits
Warning: Deprecated Parameter. The replacement is S3 native locking
(use_lockfile = true). Migration is deferred because it changes the
locking mechanism of a live backend and needs AWS access plus a
lock-migration step. Revisit on the next Terraform major bump, or the first
time the backend is recreated from scratch — whichever comes first.
The trigger above says when; this says what to run, so the person who hits
that day is not designing the migration under time pressure. Nothing here has
been executed: there is no AWS access on the development hosts, and plan /
apply in .github/workflows/terraform-apply.yml remain if: false.
Before starting
- Every client that runs
terraform initagainstenv/stagingorenv/productionmust be on a Terraform that supportsuse_lockfile(1.10 or newer).required_version = "~> 1.15.4"holds every client to the 1.15 patch line, and thehashicorp/setup-terraformpins hold CI to exactly 1.15.4; an operator's local CLI may be any 1.15.x at or above that. - No
planorapplymay be in flight. The dual-lock phase below exists so that migrated and unmigrated clients still block each other; starting it mid-run defeats that. - No IAM change is needed to begin.
TerraformStateObjectalready allowss3:GetObject,s3:PutObjectands3:DeleteObjectonenv/<environment>/*(state_prefix_arnsininfrastructure/terraform/modules/github-oidc/main.tf), and the native lock is the objectenv/<environment>/terraform.tfstate.tflockunder exactly that prefix.
Steps
- Take both locks. Add
use_lockfile = trueto thebackend "s3"block ininfrastructure/terraform/main.tf, keepingdynamodb_table, then runterraform init -reconfigurefrominfrastructure/terraform/. Terraform acquires the DynamoDB item and the.tflockobject, so a client still on the old configuration cannot run concurrently with a migrated one. - Observe both mechanisms once. With AWS access, run
terraform planand confirm the.tflockobject exists under the state key for the duration of the run and is gone afterwards, and that the DynamoDB item is still written. Record it as dated evidence: this is the only step that proves native locking works on this backend rather than in the documentation. - Drop DynamoDB from the backend once every client has completed step 1:
remove
dynamodb_tableand runterraform init -reconfigureagain. - Remove the IAM grants, then the table. Delete the
TerraformStateLockTableDescribeandTerraformStateLockItemsstatements with thestate_table_arnandstate_lock_idslocals, apply, and only then delete theagentflow-terraform-lockstable. Deleting the table first leaves a role granting DynamoDB access to nothing and hides the mistake. - Update the record. This section, the same section in
infrastructure/terraform/modules/github-oidc/README.md, andASSUMPTION-T-36-BACKENDbelow all describe DynamoDB retention as deliberate. They stop being true at step 3 and must change in that commit.
Rollback. Before step 3, remove use_lockfile and re-run terraform init -reconfigure; a leftover .tflock object is removed with aws s3 rm. After
step 3 the reverse of step 1 restores DynamoDB locking — but only while the
table still exists, which is why step 4 deletes it last.
Remaining assumptions (same class as the apply-guard retention: the tracked contract is honest, the live delivery path is not claimed):
- ASSUMPTION-T-36-BACKEND: this page does not claim that
terraform init,plan, orapplyran against the real S3 backend. DynamoDB locking is retained on the tracked configuration; a live-backend migration touse_lockfilestill needs AWS access. - ASSUMPTION-T-36-CI-LOCK — discharged 2026-09-08. The provider-lock
step in
.github/workflows/ci.ymlterraform-validaterunsterraform providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=windows_amd64followed bygit diff --exit-code .terraform.lock.hcl. It was a tracked guard with no runner behind it until the work reachedorigin; it has since passed onubuntu-latest(run 34213482386,terraform-validatesuccess, step "Provider lock covers linux_amd64"), which is the evidence that the tracked lock covers the platform CI actually runs on. What remains unobserved is only thedarwin_arm64/windows_amd64halves being used, as opposed to being regenerated identically — CI has no runner on either.
The GitHub Actions role's S3 object read/write is limited to env/staging/*
and env/production/*. Those names are exactly the workflow_dispatch
environment options in .github/workflows/terraform-apply.yml.
env/dev/* is used only by make deploy-dev with an operator's own
credentials; those objects are not readable or writable by the CI role.
s3:ListBucket is bucket-wide, so the role can see other environments'
state key names, sizes and timestamps. Narrowing s3:ListBucket with an
s3:prefix condition is a follow-up that needs verification against a
real backend init.
No bootstrap apply has been performed, so no role exists yet and
AWS_TERRAFORM_ROLE_ARN is unset — see Current readiness handoff.
The scope below is what the tracked configuration would produce.
The workflow reads exactly one AWS_TERRAFORM_ROLE_ARN repository variable
for both workflow_dispatch environments, so a single role serves both.
Because that role is shared, it holds s3:PutObject/s3:DeleteObject on
every environment listed in state_environments, so the per-environment
state keys separate state objects, not credentials. A per-environment
role would need a per-environment role variable in the workflow, which this
repository does not have.
aws_iam_openid_connect_provider.github_actions is account-global and is
owned by the state the bootstrap was run from
(env/staging/terraform.tfstate). A second environment applied from an
empty state would fail with EntityAlreadyExists; that state must first
import the existing provider. terraform import loads the configuration, so
it needs both the production backend key and the root variables
(environment, vpc_id and private_subnet_ids have no defaults):
terraform init -reconfigure -backend-config="key=env/production/terraform.tfstate"
terraform import -var-file=environments/prod.tfvars \
module.github_oidc.aws_iam_openid_connect_provider.github_actions \
arn:aws:iam::<account-id>:oidc-provider/token.actions.githubusercontent.com
Run this from a working directory inited against the production key. An
operator who has just followed Bootstrap the role is
inited against env/staging/terraform.tfstate, where the provider is already
managed; importing there does nothing useful.
After this import both states own the same account-global provider: a
terraform destroy or removal from either state deletes the trust
anchor for both, and a thumbprint change applied from one state leaves
the other drifted. Rotate the thumbprint from the staging state only.
The import covers only aws_iam_openid_connect_provider. A production
apply also creates agentflow-terraform-production plus its own
-boundary policy, and AWS_TERRAFORM_ROLE_ARN must continue to name
the single role the workflows use — the operator decides which of the two ARNs that variable keeps.
These are facts about the current repository, not a recommended design.
- Start from a trusted local machine with temporary administrator credentials in AWS.
- Change into
infrastructure/terraform. - Copy
environments/staging.tfvars.exampletoenvironments/staging.tfvarsfor the first non-production proof run. - Replace the placeholder VPC, subnet, and SNS values with real values for your AWS account.
- Run:
terraform init -backend-config="key=env/staging/terraform.tfstate"
terraform plan -var-file=environments/staging.tfvars
terraform apply -var-file=environments/staging.tfvars
The OIDC provider is account-global and lives in this staging state. A later
apply against env/production/terraform.tfstate from an empty state must
import it first (see Shared CI role and account-global OIDC provider) or it will fail with EntityAlreadyExists.
- Capture the resulting role ARN from
terraform state show module.github_oidc.aws_iam_role.github_actions.
The first apply must be local because the role does not exist yet. After the role exists, GitHub Actions can assume it through OIDC.
- Open
Settings -> Secrets and variables -> Actions -> Variables. - Create
AWS_TERRAFORM_ROLE_ARNwith the ARN output from Terraform. - Create
AWS_REGIONwith the same region used by Terraform, for exampleus-east-1. - Remove legacy long-lived credentials such as
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, andTF_AWS_ROLEif they exist. - Open
Settings -> Environmentsand createstagingandproduction. - Add required reviewers to both environments before allowing apply runs.
The workflow maps the GitHub production environment to environments/prod.tfvars and the staging environment to environments/staging.tfvars.
Before enabling the disabled plan/apply jobs, an operator can run the manual
workflow with confirm=PREFLIGHT. That path does not call terraform apply;
it only checks repository variables, real tfvars presence, and Terraform local
validation. Steps 1, 2 and 4 are static inspections of the tracked workflow
file and hold today. Step 3 needs a real federated run, so it can only be
satisfied once the if: false guards on plan/apply are lifted.
- In
terraform-apply.yml, confirm theplanandapplyjobs use the SHA-pinnedaws-actions/configure-aws-credentials(e6de054…/ v6.2.3). - Confirm the workflow uses repository variables
AWS_TERRAFORM_ROLE_ARNandAWS_REGION, not AWS access key secrets. - Inspect the AWS CloudTrail event for
AssumeRoleWithWebIdentityand confirm the federated principal istoken.actions.githubusercontent.com. - Confirm the job has
permissions.id-token: write.
If a run succeeds without AWS_ACCESS_KEY_ID and CloudTrail shows AssumeRoleWithWebIdentity, the workflow is using OIDC.
The checked-in thumbprint as of 2026-04-22 is:
dd55b4520291e276588f0dd02fafd83a7368e0fa
To refresh it:
- Follow the AWS IAM procedure for obtaining the top intermediate CA thumbprint for an OIDC provider.
- Re-check the certificate chain for
token.actions.githubusercontent.com. - Update
infrastructure/terraform/modules/github-oidc/main.tf. - Run
terraform planand apply the change with trusted credentials from the staging state (env/staging/terraform.tfstate) only. After a production import both states own the same account-global provider: applying the thumbprint change from one state leaves the other drifted onthumbprint_list, and aterraform destroyor removal from either state deletes the trust anchor for both.
Example PowerShell check used for this repository:
$tcp = [System.Net.Sockets.TcpClient]::new('token.actions.githubusercontent.com', 443)
try {
$ssl = [System.Net.Security.SslStream]::new($tcp.GetStream(), $false, ({ $true }))
$ssl.AuthenticateAsClient('token.actions.githubusercontent.com')
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($ssl.RemoteCertificate)
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chain.ChainPolicy.RevocationMode = [System.Security.Cryptography.X509Certificates.X509RevocationMode]::NoCheck
$null = $chain.Build($cert)
$chain.ChainElements | Select-Object Subject, Thumbprint
}
finally {
if ($ssl) { $ssl.Dispose() }
$tcp.Dispose()
}