Skip to content

Bring the 2023 code up to date: OIDC instead of access keys, S3-native locking, pinned versions, plan/apply split - #1

Merged
bombsdontbite merged 7 commits into
mainfrom
refresh-2026
Oct 30, 2026
Merged

bombsdontbite merged 7 commits into
mainfrom
refresh-2026

Conversation

@bombsdontbite

@bombsdontbite bombsdontbite commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

The 2023 code no longer matched current Terraform, AWS or GitHub Actions practice. This brings it up to date for the refreshed article; the README's "What changed since 2023" lists every difference.

Credentials

  • GitHub OIDC replaces the IAM user and its access key.
  • bootstrap/management creates the OIDC provider, the state bucket and one role per environment. Each role reads and writes only workspaces/<env>/* and assumes only its own target role.
  • bootstrap/target creates the role Terraform uses in each account.

State

  • use_lockfile replaces the DynamoDB lock table.
  • workspace_key_prefix = "workspaces" gives each environment its own prefix.

Pinning

  • Terraform ~> 1.17; AWS provider 6.71.0 with committed lock files.
  • Registry modules: vpc 6.7.3, autoscaling 9.3.2, alb 10.5.1, security-group 6.0.0, s3-bucket 5.17.0.

EC2 stack

  • Moved to the new module inputs.
  • Amazon Linux 2023 (AL2 support ended 2026-06-30) and IMDSv2, with user_data reading metadata through a session token.

Workflows

  • Every action is pinned to a commit SHA.
  • permissions: {} at the top; each job asks only for what it needs.
  • Reusable workflows are called by local uses: ./.github/....
  • Inputs are validated and passed to scripts through env:.
  • Plan and apply are separate jobs, with the saved tfplan passed between them as an artifact.
  • Trivy fails the run on HIGH/CRITICAL.
  • New pull-request.yml runs fmt, validate and Trivy, with results in code scanning. It touches no AWS state; the approval gate stays on apply-all, destroy-all and manual runs.

Two 2023 bugs fixed

  • The Auto Scaling service-linked role was created with the default provider, so it landed in the management account.
  • The flow-log bucket policy was never attached, because the module ignores policy unless attach_policy = true.

Verified

  • terraform fmt and validate pass on all four roots with Terraform 1.17.1.
  • Trivy at HIGH/CRITICAL exits 0, with four ignores, each explained in a code comment.
  • actionlint reports no errors.

Not verified

Nothing has been applied to AWS. Apply bootstrap/, set the five environment variables (README, Setup), then run apply-all on dev once before publishing the article.

🤖 Generated with Claude Code

bombsdontbite and others added 6 commits October 30, 2026 09:33
…>= 4.5"

Every module came from github.com/terraform-aws-modules/... with no ref, so each run used
whatever master was that day; the code from 2023 no longer plans against today's majors. The
stacks now use registry sources with exact versions (vpc 6.7.3, s3-bucket 5.17.0; the EC2 stack
follows in its own commit), the AWS provider is pinned to 6.71.0 with a committed lock file, and
Terraform to ~> 1.17 (CI runs exactly 1.17.1).

Two fixes the pin exposed in the VPC stack:
- the flow-log bucket policy was passed without attach_policy = true, which the s3-bucket module
  has always required, so the policy was never attached;
- data.aws_region.name is deprecated in provider v6; it is .region now.

Checked with Terraform 1.17.1: init -backend=false and validate pass, fmt -check -recursive is
clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed one prefix

Since Terraform 1.11 the S3 backend locks with a .tflock object next to the state
(use_lockfile = true), so the DynamoDB lock table, its cost and its IAM are gone. Workspace
states now live under workspaces/<env>/<stack>/terraform.tfstate (workspace_key_prefix) instead
of the default env:/ prefix, which lets each environment's role be limited to its own prefix,
lock objects included; the EC2 stack's terraform_remote_state reads with the same prefix. The
backend's region is the state bucket's, and a comment beside it now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…group v6, on an AMI past end of support

The stack now pins security-group 6.0.0, alb 10.5.1 and autoscaling 9.3.2 and follows their
upgrade notes:
- security-group v6 dropped named presets on the root module and the computed_* families, and
  no longer adds an allow-all egress rule. The ALB group uses the http-80 preset with
  ingress_cidr_ipv4 and may now only reach the VPC on port 80; the instances accept port 80 from
  the ALB group and leave only on 443 (packages, SSM) instead of everything.
- alb v9 replaced http_tcp_listeners/target_groups lists with maps keyed by name (listener
  forward.target_group_key), the *_timeout inputs with timeouts {}, and lb_dns_name with
  dns_name. It also turned enable_deletion_protection on by default, which would have broken
  destroy-all; a new alb_enable_deletion_protection input defaults to false.
- autoscaling v8 removed target_group_arns in favour of traffic_source_attachments, and v9
  enforces IMDSv2. The user data asked the metadata service without a token, so instances would
  have served no IP; it now takes a session token first. The cooldowns inside
  target_tracking_configuration never existed in the API and are gone.
- The launch template is the module's own, as it already was in 2023 (launch configurations
  were never used here).

Amazon Linux 2 reached end of support on 2026-06-30, so the AMI is Amazon Linux 2023 and the
user data uses dnf and systemctl. The autoscaling service-linked role had no provider set and
was created in the management account, not next to the group that uses it; it now uses
aws.target. The user data path is relative to path.module instead of the working directory.

Checked with Terraform 1.17.1: init -backend=false and validate pass. Trivy 0.76.1 finds no
HIGH/CRITICAL left except the public HTTP load balancer, which is the demo and is ignored at the
module call with the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 2023 workflow put an IAM user's access key in AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY: a
credential that never expires, works from anywhere and is shared by every environment. Now the
workflow trades GitHub's OIDC token for a short session, and nothing secret is stored.

bootstrap/management (applied once, local state) creates the GitHub OIDC provider, the state
bucket (versioned, SSE-S3, public access blocked, TLS only) and one role per environment,
github-terraform-<env>. A role trusts only this repository's jobs that run in GitHub
environment <env> (sub = repo:<owner>/<repo>:environment:<env>), may read and write only
workspaces/<env>/* in the bucket, delete only its own .tflock objects, and assume only that
environment's target role. Listing the bucket and reading the never-written default-workspace
keys are allowed because the S3 backend lists to find workspaces and init reads the current
(default) workspace's state; without list rights S3 answers that read with 403, not 404.

bootstrap/target (applied once per target account) creates the role the `target` provider
alias assumes, trusting the management roles: role chaining, as in the article, but from an
OIDC session instead of a user. It carries PowerUserAccess plus the IAM the stacks create (the
web servers' role under /ec2/ and the Auto Scaling service-linked role).

The stacks keep the double-provider scheme. The target alias now sets allowed_account_ids, so a
wrong TARGET_ACCOUNT fails before anything is planned, and names the session after the
workspace so CloudTrail shows which environment acted.

Not applied anywhere: init and validate pass with Terraform 1.17.1, Trivy 0.76.1 is clean at
HIGH/CRITICAL (the SSE-S3 choice is ignored with its reason). The S3 permissions were derived
from the backend's source at v1.17.1, not exercised against AWS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… owner/repo@main and saved the plan as "<env>.txt"

- Every action is pinned to a full commit SHA with its version beside it: checkout v7.0.1,
  configure-aws-credentials v6.3.0, setup-terraform v4.0.1 (Terraform 1.17.1),
  trivy-action v0.36.0 (Trivy v0.76.1), upload-artifact v7.0.2, download-artifact v8.0.2,
  codeql-action/upload-sarif v4.39.1. trivy-action@master ran whatever master held, and on
  2026-03-19 76 of trivy-action's 77 version tags were force-pushed to credential-stealing
  commits (GHSA-69fq-xp46-6x23); a SHA cannot be moved.
- Credentials come from GitHub OIDC (role-to-assume, id-token: write); the IAM user keys are
  gone. Role ARN, region and target are environment variables, not secrets.
- permissions: {} at the top of every workflow; jobs ask for contents: read and id-token: write,
  and the scan job for security-events: write.
- apply-all, destroy-all and the pull-request workflow call ./.github/workflows/terraform-stack.yml.
  The old bombsdontbite/multi-environment-infrastructure-provisioning/...@main named the
  repository's previous name and ran whatever main held, not the commit being run.
- terraform-stack plans in one job and applies in a later one from the same binary plan file,
  tfplan (the old name "<env>.txt" suggested text it never was), passed as a one-day artifact;
  the apply job runs only when the plan has changes. The plan is printed to the job summary.
  Inputs reach the shell only through env and are checked against the known environments,
  stacks and modes.
- concurrency moves from the job to the workflow (per environment and stack,
  cancel-in-progress: false), so the plan job and its apply job are held as one; with the group
  on a job, another run's plan could slip in between them.
- New pull-request workflow: fmt -check, init -backend=false and validate for every root module,
  Trivy failing on HIGH/CRITICAL with SARIF uploaded to code scanning, then a dev plan of vpc and
  ec2 (skipped for forks, which get no OIDC token).

Checked: every workflow parses with PyYAML; actionlint 1.7.12 with shellcheck 0.11.0 reports
nothing; the Trivy scan the pull-request job runs exits 0 locally with Trivy 0.76.1. None of it
has run on GitHub yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README listed AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY repository secrets and
TARGET_* environment secrets and nothing about creating the state bucket, lock table or roles.
It now covers what the refreshed code needs:
- setup: bootstrap/management (OIDC provider, state bucket, per-environment roles, and how to
  read the repository's OIDC subject prefix), bootstrap/target per account, the GitHub
  environments with their five variables and protection rules (stag/prod: reviewers, main
  only; dev: any branch, so pull requests can plan), and why dev must be applied once first;
- running: what each workflow does, that stag/prod are approved twice (plan job, then apply job),
  how concurrency queues, and that plan files can contain secrets and are downloadable from a
  public repository;
- workspaces and credentials: HashiCorp's advice against CLI workspaces for environments with
  different credentials, what this repository does about it (per-environment roles, state
  prefixes, accounts, allowed_account_ids), what stays shared, and when to move to separate
  root modules;
- a short table of what changed since 2023 and why, and the pinned versions.

The input table gains alb_enable_deletion_protection and loses old typos (Desctiption, bloks,
and flow_log_max_aggregation_interval missing its vpc_ prefix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

…aited on a dev approval and failed before bootstrap

pull-request.yml called terraform-stack.yml to plan vpc and ec2 in dev. In practice the run sat
at "waiting" on dev's required reviewer, and without the bootstrap roles it could only fail.
The owner chose to drop it. The workflow now runs fmt, validate and Trivy only, touches no state,
and so may cancel an older run on a new push.

The approval gate stays for apply-all, destroy-all and manual runs. The README now recommends a
required reviewer on dev too, since no pull request needs dev without one any more. It also no
longer says fork pull requests stop at the checks: the checks job's SARIF upload needs
security-events: write, which a fork's token does not get.

actionlint 1.7.12 reports no errors on the four workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bombsdontbite
bombsdontbite merged commit b11a984 into main Oct 30, 2026
2 checks passed
@bombsdontbite
bombsdontbite deleted the refresh-2026 branch October 30, 2026 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants