> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jacobpevans.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AWS bootstrap

> Admin-runnable Terraform that provisions the state bucket, GitHub OIDC trust, and per-project IAM role for a new Terraform repo. Idempotent; runs once per project.

> Run this once per new project, with admin AWS credentials. Output is everything a new repo's `backend.tf` needs.

The bootstrap is plain Terraform. The first `apply` runs with local state; once the bucket exists, you uncomment the `backend "s3"` block at the top of the file and `terraform init -migrate-state` lifts the state into the bucket the bootstrap itself just created. The bucket then hosts both its own bootstrap state (`_bootstrap/terraform.tfstate`) and the consuming repo's state (`<project>/terraform.tfstate`).

## Prerequisites

* Admin AWS credentials in the shell — `aws sts get-caller-identity` returns an admin ARN.

* Terraform ≥ 1.10 or OpenTofu ≥ 1.10 on PATH.

* The new GitHub repo (`<github-org>/<github-repo>`) already exists.

* The GitHub Actions OIDC provider exists in the AWS account. Check with:

  ```bash theme={null}
  aws iam list-open-id-connect-providers \
    --query 'OpenIDConnectProviderList[?contains(Arn, `token.actions.githubusercontent.com`)]'
  ```

  If the result is empty, create it once per account (one-time, account-wide):

  ```bash theme={null}
  aws iam create-open-id-connect-provider \
    --url https://token.actions.githubusercontent.com \
    --client-id-list sts.amazonaws.com
  ```

  AWS verifies the GitHub Actions issuer's certificate chain automatically — no manual thumbprint is needed.

* Each human operator has an IAM user with MFA enabled, and a policy granting only `sts:AssumeRole` on `arn:aws:iam::<account-id>:role/tf-*` (no direct resource permissions). Operator IAM user creation is a per-operator one-time step, separate from per-project bootstrap.

## Where this lives

The recommended layout is one directory per project inside a single admin-owned repo (suggested name: `terraform-aws-foundation`):

```text theme={null}
terraform-aws-foundation/
├── bootstrap/
│   ├── proxmox/
│   │   ├── main.tf          # this file
│   │   └── terraform.tfvars # per-project values
│   ├── unifi/
│   │   ├── main.tf
│   │   └── terraform.tfvars
│   └── ...
└── README.md
```

Each per-project directory is independent: its own state object lives in its own bucket, so projects cannot affect each other even by accident.

## The bootstrap module

The Terraform code lives in [`dryvist/terraform-aws-template`][repo] (Apache-2.0, public). Each per-project bootstrap directory is a small root module that wires the published module to the project's values:

```hcl theme={null}
terraform {
  required_version = ">= 1.10"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  # First `terraform apply` runs with local state. Once the bucket exists,
  # uncomment this block (substitute the bucket name the apply emits) and
  # run `terraform init -migrate-state` to lift state into the bucket.
  #
  # backend "s3" {
  #   bucket       = "tfstate-<project>-<account-id>"
  #   key          = "_bootstrap/terraform.tfstate"
  #   region       = "us-east-1"
  #   use_lockfile = true
  #   encrypt      = true
  # }
}

provider "aws" {
  region = "us-east-1"
}

module "state_backend" {
  source = "git::https://github.com/dryvist/terraform-aws-template.git?ref=v0.1.0"

  project        = "proxmox"
  github_org     = "<github-org>"
  github_repo    = "terraform-proxmox"
  branch_pattern = "main"

  operator_user_arns = [
    "arn:aws:iam::<account-id>:user/<operator>",
  ]
}

output "backend_config"   { value = module.state_backend.backend_config }
output "tf_role_arn"      { value = module.state_backend.tf_role_arn }
output "state_bucket"     { value = module.state_backend.state_bucket }
output "state_key_prefix" { value = module.state_backend.state_key_prefix }
```

Full input / output reference and the list of underlying AWS resources live in the module repo's [README][readme]. The module pins to a tagged release (`v0.1.0` above) — breaking changes ship as new majors so existing bootstraps stay valid until you re-pin.

The S3-native lock object (`<project>/terraform.tfstate.tflock`) is just another S3 object under the same prefix as state — no separate IAM permission required, no DynamoDB table.

[repo]: https://github.com/dryvist/terraform-aws-template

[readme]: https://github.com/dryvist/terraform-aws-template/blob/main/README.md

## Bootstrap the chicken-and-egg

<Steps>
  <Step title="Apply locally">
    With the module block above pointing at your project's values:

    ```bash theme={null}
    terraform init      # local state — no backend block yet
    terraform apply
    ```

    Confirm the apply. `terraform output backend_config` emits the ready-to-paste `backend "s3" {}` block for the consuming repo (`terraform output -raw backend_config > /tmp/backend.tf` to ship it straight to a file).
  </Step>

  <Step title="Uncomment the backend block">
    In `main.tf`, uncomment the `backend "s3"` block at the top of the `terraform {}` block and substitute the outputs the apply just produced:

    ```hcl theme={null}
    backend "s3" {
      bucket       = "tfstate-proxmox-<account-id>"
      key          = "_bootstrap/terraform.tfstate"
      region       = "us-east-1"
      use_lockfile = true
      encrypt      = true
    }
    ```

    `encrypt = true` instructs the client to send the SSE header on every PutObject. The bucket's default SSE-S3 encryption is already configured by the module above — no `kms_key_id` is needed because there is no KMS key.
  </Step>

  <Step title="Migrate state into the bucket">
    ```bash theme={null}
    terraform init -migrate-state
    ```

    Terraform prompts to copy the local state file into the bucket. Confirm. Then delete the local artefacts:

    ```bash theme={null}
    rm terraform.tfstate terraform.tfstate.backup
    ```

    The bootstrap is now self-hosting in the bucket it created. Subsequent `terraform plan` / `terraform apply` runs against the bootstrap (for example to widen `branch_pattern` or add another operator) work like any other Terraform module.
  </Step>
</Steps>

## Verify

```bash theme={null}
# State bucket exists and contains the bootstrap state.
aws s3 ls s3://tfstate-<project>-<account-id>/_bootstrap/

# Role exists with the expected name.
aws iam get-role --role-name tf-<project>

# Outputs — feed these into the consuming repo's backend.tf.
terraform output -json
```

If all three succeed, the consuming repo can immediately set up its [backend.tf](/infrastructure/tofu-aws/consuming-repo) and run its first `terraform plan`.

```mermaid theme={null}
%%{init: {'theme':'base','look':'handDrawn','themeVariables':{'fontFamily':'Geist','fontSize':'14px','primaryColor':'#102937','primaryTextColor':'#F4EFE6','primaryBorderColor':'#4FB3A9','lineColor':'#4FB3A9','secondaryColor':'#0B1D2A','tertiaryColor':'#1A2A38','clusterBkg':'rgba(79,179,169,0.08)','clusterBorder':'#4FB3A9'}}}%%
flowchart LR
  Admin([Admin shell])
  Apply([terraform apply<br/>local state])
  Bucket[(State bucket created)]
  Migrate([init -migrate-state])
  Done([Self-hosting])

  Admin --> Apply --> Bucket --> Migrate --> Done

  classDef src  fill:#102937,stroke:#E06B4A,stroke-width:2px,color:#F4EFE6;
  classDef hop  fill:#102937,stroke:#4FB3A9,stroke-width:2px,color:#F4EFE6;
  classDef sink fill:#102937,stroke:#F4EFE6,stroke-width:2px,color:#F4EFE6;

  class Admin src
  class Apply,Migrate hop
  class Bucket,Done sink

  linkStyle 0,1,2,3 stroke:#F4EFE6,stroke-width:1.5px;
```

## Where to go next

<CardGroup cols={2}>
  <Card title="OpenTofu on AWS overview" icon="diagram-project" href="/infrastructure/tofu-aws/overview">
    The isolation model and naming conventions this bootstrap implements.
  </Card>

  <Card title="Set up the consuming repo" icon="folder-tree" href="/infrastructure/tofu-aws/consuming-repo">
    What the new repo drops in next to use the outputs above.
  </Card>

  <Card title="OpenTofu check placement" icon="list-check" href="/infrastructure/tofu-check-placement">
    Where every Terraform / OpenTofu command runs — pre-commit vs CI.
  </Card>

  <Card title="aws-vault profile mechanics" icon="key" href="/security/tools/aws-vault">
    Operator-side credential management for the role this bootstrap created.
  </Card>
</CardGroup>
