# Opt-in GitLab CI sample

This directory is deliberately not included by the root Pages pipeline. Copy or include it only after an AWS administrator has created or approved the account-level GitLab OIDC provider and the project has protected `main` and the `production` environment.

Set these protected CI/CD variables before enabling `IAC_PIPELINE=true`:

- `AWS_ROLE_ARN` - output `ci_role_arn` after the optional OIDC provider is configured.
- `EXPECTED_AWS_ACCOUNT_ID` - the approved 12-digit account ID.
- `FOUNDATION_BACKEND_CONFIG` - a file variable containing the reviewed S3 backend configuration.
- `TF_VAR_budget_alert_email` - a real budget recipient; the foundation rejects example and reserved placeholder domains.

The job creates a short-lived GitLab ID token for `sts.amazonaws.com`, writes a temporary named AWS profile, and verifies STS identity. It does not use stored AWS access keys. The sample role trust pins the issuer audience and `project_path:...:ref:main`; replace the generic issuer, project path, and ref before creating it.

The Linux AMD64 CLI versions and SHA256 values are pinned together. Each ZIP is checked before `unzip` against the official [Terraform 1.16.0 checksum manifest](https://releases.hashicorp.com/terraform/1.16.0/terraform_1.16.0_SHA256SUMS) or [OpenTofu 1.12.6 checksum manifest](https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_SHA256SUMS). A version override is rejected instead of downloading a mutable, unverified tool.

`plan` retains the binary plan and its commit, tool, region, account, and backend-config digest for one hour. The protected manual production job checks that metadata and applies only `tfplan`. A saved plan can become stale if another run changes state, so a failed apply is evidence to review, not a reason to create a fresh automatic plan. `drift` uses `-refresh-only`, reports differences, and cannot mutate infrastructure. There is intentionally no cleanup or automatic apply job.

The optional CI role has a reviewed foundation policy: exact state object and lock access, bootstrap-state KMS access, and the provider lifecycle calls demonstrated by the foundation resources. Logs, the workload KMS key and alias, the metadata secret, and the monthly budget use exact managed ARN patterns. Within those named services, `kms:CreateKey` is the only genuinely unscopable create-before-ARN operation. `logs:DescribeLogGroups`, `kms:ListAliases`, and the required billing-console compatibility actions also have no resource-level scope. The regional EC2 lifecycle remains on `Resource = "*"` because its create and dependency calls span generated VPC, subnet, route, gateway, endpoint, address, security-group, and flow-log identifiers; `aws:RequestedRegion` limits those requests. The CI role manages infrastructure and secret metadata only. It cannot read secret values or write flow-log events. IAM role-policy operations are limited to the three course-managed role ARNs, and `PassRole` is limited to VPC Flow Logs. Bootstrap owns CI-role creation and deletion; the CI role can only read and reconcile its bounded inline policy, tags, and trust policy. The pre-created permissions boundary is owned by the bootstrap administrator and constrains every CI role permission. The local/admin bootstrap remains separate.

The $10 learning and $100 production budgets are separate account-wide monthly alert thresholds, not profile-attributed cost controls. Do not deploy both examples in one account unless overlapping account-wide alerts are intentional.

Configure GitLab protected production deployment approvals separately in project and environment settings. The manual YAML job and protected ref gate do not replace required deployment approvals. The plan artifacts include the generated lock file and CLI-version metadata; apply initializes it read-only and compares the exact CLI/tool, commit, account, region, backend digest, and saved plan before mutation.
