Goal: create the secure S3 bucket (and KMS key) that will hold the remote state for one AWS account. This is the first thing you run in any account, and it is the only layer that uses local state.
Read along in bootstrap/main.tf. It creates, in order:
-
A KMS key + alias to encrypt the state.
resource "aws_kms_key" "state" { description = "${var.project}-${var.environment} OpenTofu state encryption" deletion_window_in_days = 14 enable_key_rotation = true # automatically rotate the key } -
The S3 bucket itself, plus several hardening resources applied to it. Notice that AWS S3 security is configured as separate resources that each point back at the bucket - this is normal in the AWS provider:
aws_s3_bucket_versioning- keep old versions of the state, so you can recover if it's corrupted.aws_s3_bucket_server_side_encryption_configuration- encrypt objects with the KMS key from step 1.aws_s3_bucket_public_access_block- make it impossible to accidentally make this bucket public (all four switches on).aws_s3_bucket_lifecycle_configuration- automatically delete state versions older than 90 days so the bucket doesn't grow forever.aws_s3_bucket_policy- a bucket policy that denies any non-TLS (unencrypted in transit) access.
-
The TLS-only policy is built with a
data "aws_iam_policy_document"block. This is the idiomatic way to write IAM/bucket policies in HCL instead of raw JSON - you describe the policy in HCL and the data source renders the JSON:data "aws_iam_policy_document" "state_bucket" { statement { sid = "DenyInsecureTransport" effect = "Deny" principals { type = "*" identifiers = ["*"] } actions = ["s3:*"] resources = [aws_s3_bucket.state.arn, "${aws_s3_bucket.state.arn}/*"] condition { test = "Bool" variable = "aws:SecureTransport" values = ["false"] # i.e. deny when the request is NOT over TLS } } }
The outputs (bootstrap/outputs.tf) then expose the bucket
name, the region, and crucially the KMS key ARN - the value every other
layer's backend needs.
The one-workspace-per-account trick
A workspace in OpenTofu is a way to keep multiple separate states from the
same code. Bootstrap uses one workspace per account
(management/dev/staging/prod), so a single bootstrap/ directory remembers
the backend it created in every account. The Makefile does this for you:
bootstrap-apply: bootstrap-init
$(TOFU) -chdir=bootstrap workspace select -or-create $(ENV)
$(TOFU) -chdir=bootstrap apply -input=false -var-file=$(ENV).tfvarsSo make bootstrap-apply ENV=dev selects (or creates) the dev workspace and
applies with dev.tfvars. Run it once per account, switching your AWS credentials
to the matching account each time.
-var-file=dev.tfvarssupplies the input variables from a file. The.tfvarsfile holds the real account ID and a globally-unique bucket name. Real*.tfvarsfiles are git-ignored (they hold account IDs); only*.examplecopies are committed.