Skip to content

Commit d5aefb3

Browse files
Benbentwoclaude
andauthored
Add support for multiple Terraform state backends (#62)
This change adds backward-compatible support for configuring multiple Terraform state backends, each with its own set of SSO permission sets. **Changes:** - Add `tf_access_additional_backends` map variable for configuring multiple backends - Each backend creates three permission sets: - TerraformPlanAccess-{BackendName} - TerraformApplyAccess-{BackendName} - TerraformStateAccess-{BackendName} - Existing single backend variables remain unchanged for backward compatibility - Backend names are title-cased for permission set names (e.g., "core" -> "Core") **Use Case:** Organizations with separate state backends (e.g., core vs platform infrastructure) can now grant fine-grained SSO access per backend, ensuring proper isolation while maintaining appropriate access levels (plan/apply/state). **Example Configuration:** ```hcl tf_access_additional_backends = { core = { bucket_arn = "arn:aws:s3:::example-core-tfstate" dynamodb_table_arn = "arn:aws:dynamodb:us-east-1:123456789012:table/example-core-tfstate-lock" role_arn = "arn:aws:iam::123456789012:role/example-core-gbl-root-tfstate" } plat = { bucket_arn = "arn:aws:s3:::example-plat-tfstate" role_arn = "arn:aws:iam::123456789012:role/example-plat-gbl-root-tfstate" } } ``` Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 4b61d59 commit d5aefb3

File tree

2 files changed

+216
-0
lines changed

2 files changed

+216
-0
lines changed

src/main.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ module "permission_sets" {
8282
local.terraform_plan_access_permission_set,
8383
local.terraform_apply_access_permission_set,
8484
local.terraform_state_access_permission_set,
85+
local.terraform_plan_access_additional_permission_sets,
86+
local.terraform_apply_access_additional_permission_sets,
87+
local.terraform_state_access_additional_permission_sets,
8588
)
8689

8790
context = module.this.context

src/policy-TerraformAccess.tf

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,50 @@ variable "tf_access_role_arn" {
1717
default = ""
1818
}
1919

20+
variable "tf_access_additional_backends" {
21+
type = map(object({
22+
bucket_arn = string
23+
dynamodb_table_arn = optional(string, "")
24+
role_arn = string
25+
}))
26+
description = <<-EOT
27+
Map of additional Terraform state backends to grant SSO permission sets access to.
28+
Each entry creates three permission sets: TerraformPlanAccess-<key>, TerraformApplyAccess-<key>, and TerraformStateAccess-<key>.
29+
30+
The map key should be a descriptive name for the backend (e.g., "core", "plat", "prod").
31+
This key will be title-cased and appended to the permission set names with a hyphen.
32+
33+
Example:
34+
```
35+
tf_access_additional_backends = {
36+
core = {
37+
bucket_arn = "arn:aws:s3:::example-core-tfstate"
38+
dynamodb_table_arn = "arn:aws:dynamodb:us-east-1:123456789012:table/example-core-tfstate-lock"
39+
role_arn = "arn:aws:iam::123456789012:role/example-core-gbl-root-tfstate"
40+
}
41+
plat = {
42+
bucket_arn = "arn:aws:s3:::example-plat-tfstate"
43+
role_arn = "arn:aws:iam::123456789012:role/example-plat-gbl-root-tfstate"
44+
}
45+
}
46+
```
47+
EOT
48+
default = {}
49+
}
50+
2051
locals {
2152
tf_access_enabled = module.this.enabled && var.tf_access_bucket_arn != "" && var.tf_access_role_arn != ""
2253

54+
# Additional backends access
55+
tf_access_additional_backends_enabled = module.this.enabled && length(var.tf_access_additional_backends) > 0
56+
57+
# Helper to title-case the backend names for permission set names
58+
# "core" -> "Core", "plat" -> "Plat", "prod-us-east-1" -> "ProdUsEast1"
59+
backend_names_titlecase = {
60+
for key, config in var.tf_access_additional_backends :
61+
key => join("", [for part in split("-", key) : title(part)])
62+
}
63+
2364
# Terraform Plan Access permission set
2465
terraform_plan_access_permission_set = local.tf_access_enabled ? [{
2566
name = "TerraformPlanAccess",
@@ -55,6 +96,46 @@ locals {
5596
policy_attachments = []
5697
customer_managed_policy_attachments = []
5798
}] : []
99+
100+
# Additional backends permission sets
101+
terraform_plan_access_additional_permission_sets = local.tf_access_additional_backends_enabled ? [
102+
for key, config in var.tf_access_additional_backends : {
103+
name = "TerraformPlanAccess-${local.backend_names_titlecase[key]}"
104+
description = "Allow read-only access to Terraform state for planning (${key} backend)"
105+
relay_state = ""
106+
session_duration = var.session_duration
107+
tags = {}
108+
inline_policy = data.aws_iam_policy_document.terraform_plan_access_additional[key].json
109+
policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/ReadOnlyAccess"]
110+
customer_managed_policy_attachments = []
111+
}
112+
] : []
113+
114+
terraform_apply_access_additional_permission_sets = local.tf_access_additional_backends_enabled ? [
115+
for key, config in var.tf_access_additional_backends : {
116+
name = "TerraformApplyAccess-${local.backend_names_titlecase[key]}"
117+
description = "Allow full access to Terraform state and account for applying changes (${key} backend)"
118+
relay_state = ""
119+
session_duration = var.session_duration
120+
tags = {}
121+
inline_policy = data.aws_iam_policy_document.terraform_apply_access_additional[key].json
122+
policy_attachments = ["arn:${local.aws_partition}:iam::aws:policy/AdministratorAccess"]
123+
customer_managed_policy_attachments = []
124+
}
125+
] : []
126+
127+
terraform_state_access_additional_permission_sets = local.tf_access_additional_backends_enabled ? [
128+
for key, config in var.tf_access_additional_backends : {
129+
name = "TerraformStateAccess-${local.backend_names_titlecase[key]}"
130+
description = "Allow read/write access to Terraform state backend only (${key} backend)"
131+
relay_state = ""
132+
session_duration = var.session_duration
133+
tags = {}
134+
inline_policy = data.aws_iam_policy_document.terraform_state_access_additional[key].json
135+
policy_attachments = []
136+
customer_managed_policy_attachments = []
137+
}
138+
] : []
58139
}
59140

60141
# Terraform Plan Access - Read-only state access, read-only account access
@@ -187,3 +268,135 @@ data "aws_iam_policy_document" "terraform_state_access" {
187268
resources = [var.tf_access_role_arn]
188269
}
189270
}
271+
272+
# Additional backends policy documents
273+
274+
# Terraform Plan Access - Read-only state access for additional backends
275+
data "aws_iam_policy_document" "terraform_plan_access_additional" {
276+
for_each = local.tf_access_additional_backends_enabled ? var.tf_access_additional_backends : {}
277+
278+
# Read-only access to Terraform state S3 bucket
279+
statement {
280+
sid = "TerraformStateBackendS3BucketReadOnly"
281+
effect = "Allow"
282+
actions = [
283+
"s3:ListBucket",
284+
"s3:GetObject",
285+
]
286+
resources = [each.value.bucket_arn, "${each.value.bucket_arn}/*"]
287+
}
288+
289+
# Allow assuming the Terraform state backend role
290+
statement {
291+
sid = "TerraformStateBackendAssumeRole"
292+
effect = "Allow"
293+
actions = [
294+
"sts:AssumeRole",
295+
"sts:TagSession",
296+
"sts:SetSourceIdentity",
297+
]
298+
resources = [each.value.role_arn]
299+
}
300+
301+
# Allow EC2 DescribeRegions - required by many Terraform modules for region validation
302+
statement {
303+
sid = "EC2DescribeRegions"
304+
effect = "Allow"
305+
actions = [
306+
"ec2:DescribeRegions",
307+
]
308+
resources = ["*"]
309+
}
310+
}
311+
312+
# Terraform Apply Access - Read/write state access for additional backends
313+
data "aws_iam_policy_document" "terraform_apply_access_additional" {
314+
for_each = local.tf_access_additional_backends_enabled ? var.tf_access_additional_backends : {}
315+
316+
statement {
317+
sid = "TerraformStateBackendS3Bucket"
318+
effect = "Allow"
319+
actions = [
320+
"s3:ListBucket",
321+
"s3:GetObject",
322+
"s3:PutObject",
323+
]
324+
resources = [each.value.bucket_arn, "${each.value.bucket_arn}/*"]
325+
}
326+
327+
# Conditional DynamoDB access (only if table ARN is provided)
328+
dynamic "statement" {
329+
for_each = each.value.dynamodb_table_arn != "" ? [1] : []
330+
331+
content {
332+
sid = "TerraformStateBackendDynamoDbTable"
333+
effect = "Allow"
334+
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
335+
resources = [each.value.dynamodb_table_arn]
336+
}
337+
}
338+
339+
# Allow assuming the Terraform state backend role
340+
statement {
341+
sid = "TerraformStateBackendAssumeRole"
342+
effect = "Allow"
343+
actions = [
344+
"sts:AssumeRole",
345+
"sts:TagSession",
346+
"sts:SetSourceIdentity",
347+
]
348+
resources = [each.value.role_arn]
349+
}
350+
351+
# Allow EC2 DescribeRegions - required by many Terraform modules for region validation
352+
statement {
353+
sid = "EC2DescribeRegions"
354+
effect = "Allow"
355+
actions = [
356+
"ec2:DescribeRegions",
357+
]
358+
resources = ["*"]
359+
}
360+
}
361+
362+
# Terraform State Access - Read/write state only for additional backends
363+
data "aws_iam_policy_document" "terraform_state_access_additional" {
364+
for_each = local.tf_access_additional_backends_enabled ? var.tf_access_additional_backends : {}
365+
366+
# Read/write access to Terraform state S3 bucket
367+
statement {
368+
sid = "TerraformStateBackendS3Bucket"
369+
effect = "Allow"
370+
actions = [
371+
"s3:ListBucket",
372+
"s3:GetObject",
373+
"s3:PutObject",
374+
"s3:DeleteObject",
375+
]
376+
resources = [each.value.bucket_arn, "${each.value.bucket_arn}/*"]
377+
}
378+
379+
# DynamoDB table access for state locking (if configured)
380+
dynamic "statement" {
381+
for_each = each.value.dynamodb_table_arn != "" ? [1] : []
382+
383+
content {
384+
sid = "TerraformStateBackendDynamoDbTable"
385+
effect = "Allow"
386+
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:DeleteItem"]
387+
resources = [each.value.dynamodb_table_arn]
388+
}
389+
}
390+
391+
# Allow assuming the Terraform state backend role
392+
statement {
393+
sid = "TerraformStateBackendAssumeRole"
394+
effect = "Allow"
395+
actions = [
396+
"sts:AssumeRole",
397+
"sts:TagSession",
398+
"sts:SetSourceIdentity",
399+
]
400+
resources = [each.value.role_arn]
401+
}
402+
}

0 commit comments

Comments
 (0)