Skip to content

Commit 46d934e

Browse files
feat(infra): configure Terraform remote state with S3, DynamoDB lock, and KMS encryption
Closes VertexChainLabs#165 - Create infrastructure/terraform/backend.tf with: - S3 bucket for remote state with versioning and KMS-SSE encryption - DynamoDB table for state locking (PAY_PER_REQUEST, PITR, SSE) - KMS key with key rotation for state encryption - Lifecycle rules: keep 10 noncurrent versions for 90 days, abort stale multipart uploads after 7 days - Move terraform backend block from providers.tf to backend.tf - Use unique data.aws_caller_identity.state_backend to avoid collision with conditional data source in backup-vaults.tf - Backend block is initially commented out to support standard bootstrap flow
1 parent 7c17074 commit 46d934e

2 files changed

Lines changed: 177 additions & 15 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# =============================================================================
2+
# Terraform Remote State Backend
3+
#
4+
# Uses S3 for state storage with KMS encryption, DynamoDB for state locking to
5+
# prevent concurrent modifications, and lifecycle rules to manage state history.
6+
#
7+
# The S3 bucket and DynamoDB table are created by this configuration via a
8+
# bootstrap process. After initial creation, uncomment the backend block below
9+
# and run `terraform init -migrate-state` to migrate local state to the remote
10+
# backend.
11+
# =============================================================================
12+
13+
# ---------------------------------------------------------------------------
14+
# 1. TERRAFORM BACKEND — uncomment after bootstrap
15+
# ---------------------------------------------------------------------------
16+
# terraform {
17+
# backend "s3" {
18+
# bucket = aws_s3_bucket.terraform_state.bucket
19+
# key = "vertexchain/terraform.tfstate"
20+
# region = var.region
21+
# encrypt = true
22+
# kms_key_id = aws_kms_key.terraform_state.arn
23+
# dynamodb_table = aws_dynamodb_table.terraform_state_lock.name
24+
#
25+
# # Workspace isolation — state paths become:
26+
# # env:/dev/vertexchain/terraform.tfstate
27+
# # env:/staging/vertexchain/terraform.tfstate
28+
# # env:/prod/vertexchain/terraform.tfstate
29+
# workspace_key_prefix = "env:"
30+
# }
31+
# }
32+
33+
# ---------------------------------------------------------------------------
34+
# 2. KMS KEY — encrypts the Terraform state at rest
35+
# ---------------------------------------------------------------------------
36+
resource "aws_kms_key" "terraform_state" {
37+
description = "KMS key for encrypting Terraform remote state in S3"
38+
deletion_window_in_days = 30
39+
enable_key_rotation = true
40+
41+
policy = jsonencode({
42+
Version = "2012-10-17"
43+
Statement = [
44+
{
45+
Sid = "Enable IAM User Permissions"
46+
Effect = "Allow"
47+
Principal = {
48+
AWS = "arn:aws:iam::${data.aws_caller_identity.state_backend.account_id}:root"
49+
}
50+
Action = "kms:*"
51+
Resource = "*"
52+
},
53+
{
54+
Sid = "Allow S3 to use the key for state encryption"
55+
Effect = "Allow"
56+
Principal = {
57+
Service = "s3.amazonaws.com"
58+
}
59+
Action = [
60+
"kms:GenerateDataKey",
61+
"kms:Decrypt"
62+
]
63+
Resource = "*"
64+
}
65+
]
66+
})
67+
68+
tags = local.common_tags
69+
}
70+
71+
resource "aws_kms_alias" "terraform_state" {
72+
name = "alias/${local.name_prefix}-terraform-state"
73+
target_key_id = aws_kms_key.terraform_state.key_id
74+
}
75+
76+
# ---------------------------------------------------------------------------
77+
# 3. S3 BUCKET — stores Terraform state files
78+
# ---------------------------------------------------------------------------
79+
resource "aws_s3_bucket" "terraform_state" {
80+
bucket = "${var.project_name}-terraform-state-${data.aws_caller_identity.state_backend.account_id}"
81+
82+
tags = merge(local.common_tags, {
83+
Purpose = "terraform-remote-state"
84+
})
85+
}
86+
87+
# Block all public access
88+
resource "aws_s3_bucket_public_access_block" "terraform_state" {
89+
bucket = aws_s3_bucket.terraform_state.id
90+
block_public_acls = true
91+
block_public_policy = true
92+
ignore_public_acls = true
93+
restrict_public_buckets = true
94+
}
95+
96+
# Enable versioning so every state change is recoverable
97+
resource "aws_s3_bucket_versioning" "terraform_state" {
98+
bucket = aws_s3_bucket.terraform_state.id
99+
versioning_configuration {
100+
status = "Enabled"
101+
}
102+
}
103+
104+
# KMS server-side encryption (default for all objects)
105+
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
106+
bucket = aws_s3_bucket.terraform_state.id
107+
108+
rule {
109+
apply_server_side_encryption_by_default {
110+
sse_algorithm = "aws:kms"
111+
kms_master_key_id = aws_kms_key.terraform_state.arn
112+
}
113+
bucket_key_enabled = true
114+
}
115+
}
116+
117+
# ---------------------------------------------------------------------------
118+
# 4. LIFECYCLE RULES — manage state file history
119+
# - Keep the latest N noncurrent versions for rollback
120+
# - Expire older versions after 90 days
121+
# ---------------------------------------------------------------------------
122+
resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {
123+
bucket = aws_s3_bucket.terraform_state.id
124+
125+
rule {
126+
id = "state-version-lifecycle"
127+
status = "Enabled"
128+
129+
# Keep up to 10 noncurrent versions for safe rollback
130+
noncurrent_version_expiration {
131+
noncurrent_days = 90
132+
newer_noncurrent_versions = 10
133+
}
134+
135+
# Abort incomplete multipart uploads (e.g. from interrupted applies)
136+
abort_incomplete_multipart_upload {
137+
days_after_initiation = 7
138+
}
139+
}
140+
}
141+
142+
# ---------------------------------------------------------------------------
143+
# 5. DYNAMODB TABLE — state locking to prevent concurrent modifications
144+
# ---------------------------------------------------------------------------
145+
resource "aws_dynamodb_table" "terraform_state_lock" {
146+
name = "${var.project_name}-terraform-state-locks"
147+
billing_mode = "PAY_PER_REQUEST"
148+
hash_key = "LockID"
149+
150+
attribute {
151+
name = "LockID"
152+
type = "S"
153+
}
154+
155+
# Enable point-in-time recovery so lock history is preserved
156+
point_in_time_recovery {
157+
enabled = true
158+
}
159+
160+
server_side_encryption {
161+
enabled = true
162+
}
163+
164+
tags = merge(local.common_tags, {
165+
Purpose = "terraform-state-lock"
166+
})
167+
}
168+
169+
# ---------------------------------------------------------------------------
170+
# 6. DATA SOURCE — needed for KMS policy and globally unique bucket name.
171+
# Uses a distinct name ("state_backend") to avoid colliding with the
172+
# conditional data "aws_caller_identity" "current" in backup-vaults.tf.
173+
# ---------------------------------------------------------------------------
174+
data "aws_caller_identity" "state_backend" {}

infrastructure/terraform/providers.tf

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
terraform {
22
required_version = ">= 1.5.0"
33

4-
# The S3 backend automatically isolates state per workspace when using
5-
# workspace_key_prefix. State paths will be:
6-
# env:/dev/vertexchain/terraform.tfstate
7-
# env:/staging/vertexchain/terraform.tfstate
8-
# env:/prod/vertexchain/terraform.tfstate
9-
# The default workspace continues to use the key directly:
10-
# vertexchain/terraform.tfstate
11-
backend "s3" {
12-
bucket = "vertexchain-terraform-state"
13-
key = "vertexchain/terraform.tfstate"
14-
workspace_key_prefix = "env:"
15-
region = "us-east-1"
16-
encrypt = true
17-
dynamodb_table = "vertexchain-terraform-locks"
18-
}
4+
# Backend configuration has been moved to backend.tf.
5+
# See backend.tf for S3 remote state, DynamoDB locking,
6+
# KMS encryption, and lifecycle rules.
197

208
required_providers {
219
aws = {

0 commit comments

Comments
 (0)