Skip to content

Add support for multiple Terraform state backends#62

Merged
Benbentwo merged 2 commits intomainfrom
feature/multiple-state-backends-support
Feb 9, 2026
Merged

Add support for multiple Terraform state backends#62
Benbentwo merged 2 commits intomainfrom
feature/multiple-state-backends-support

Conversation

@Benbentwo
Copy link
Contributor

@Benbentwo Benbentwo commented Feb 4, 2026

Summary

Add backward-compatible support for configuring multiple Terraform state backends, each with its own set of SSO permission sets.

Problem

Organizations with separate state backends (e.g., core infrastructure vs platform infrastructure) previously had to grant the same SSO permission sets access to all backends. This created security concerns as users/groups would have access to state backends they shouldn't.

Solution

Introduces a new tf_access_additional_backends map variable that allows configuring multiple backends, where each backend creates its own set of three permission sets:

  • TerraformPlanAccess-{BackendName} - Read-only state access with ReadOnlyAccess AWS policy
  • TerraformApplyAccess-{BackendName} - Read/write state access with AdministratorAccess AWS policy
  • TerraformStateAccess-{BackendName} - Read/write state access only (no AWS account permissions)

Key Features

Backward compatible - Existing single backend variables (tf_access_bucket_arn, tf_access_role_arn) continue to work unchanged
Flexible DynamoDB - DynamoDB table ARN is optional (supports S3 state locking)
Clear naming - Backend names are title-cased for permission set names (e.g., "core" → "Core", "prod-east" → "ProdEast")
Fine-grained access - Each backend has independent permission sets for granular access control

Example Configuration

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"
    # DynamoDB table omitted - using S3 state locking
  }
}

This creates permission sets:

  • TerraformPlanAccess-Core, TerraformApplyAccess-Core, TerraformStateAccess-Core
  • TerraformPlanAccess-Plat, TerraformApplyAccess-Plat, TerraformStateAccess-Plat

Implementation Details

Files Changed:

  • src/policy-TerraformAccess.tf - Added new variable, locals, and IAM policy documents
  • src/main.tf - Wired new permission set lists into module configuration

Technical Approach:

  • Uses for_each over the backends map to generate permission sets dynamically
  • Title-cases map keys for clean permission set naming
  • Conditional DynamoDB statements based on whether table ARN is provided
  • Mirrors existing single-backend policy structure for consistency

Testing

Tested in production with two backends (core + plat):

  • ✅ Permission sets created successfully
  • ✅ Atmos profiles updated to use backend-specific permission sets
  • ✅ SSO users can successfully assume roles and access appropriate state buckets
  • ✅ Backward compatibility verified - original permission sets still work

Migration Path

  1. Deploy component with new variable (no breaking changes)
  2. Add tf_access_additional_backends to stack configs as needed
  3. Update Atmos profiles to use new backend-specific permission sets
  4. (Optional) Migrate existing single-backend configs to map format

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for configuring additional Terraform backends with customizable state access permissions.
    • New permission sets enable granular access control for Terraform plan, apply, and state operations across multiple backends.
    • Enhanced IAM policy framework for multi-backend state management with per-backend S3 bucket, DynamoDB, and role access controls.

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>
@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

Warning

Rate limit exceeded

@Benbentwo has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 58 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

The PR extends Terraform access management by introducing a configurable input variable for additional backends. It generates three new permission set categories (Plan, Apply, State) per backend with corresponding IAM policies, conditionally including DynamoDB permissions, and concatenates these into the module's permission sets list.

Changes

Cohort / File(s) Summary
Additional Backends Variable Definition
src/policy-TerraformAccess.tf
Added new public variable tf_access_additional_backends accepting map of backend configurations with S3 bucket, optional DynamoDB, and role ARNs; introduced helper local for title-casing backend names and enabled flag to gate functionality.
Per-Backend Permission Set Generation
src/policy-TerraformAccess.tf
Introduced three new permission-set generation paths (terraform_plan_access_additional_permission_sets, terraform_apply_access_additional_permission_sets, terraform_state_access_additional_permission_sets) that dynamically create permission sets per backend with appropriately-named sets, inline policies, and policy attachments.
Per-Backend Policy Documents
src/policy-TerraformAccess.tf
Added three new data blocks for IAM policy documents that conditionally include DynamoDB statements (when applicable) and consistently provide S3 access, role assumption, and EC2 region permissions for each backend iteration.
Module Permission Set Concatenation
src/main.tf
Updated permissionSets module input to concatenate the three new additional backend permission set locals with existing permission set definitions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

minor, needs-test

Suggested reviewers

  • osterman
  • milldr
  • aknysh

Poem

🐰 New backends hop into view,
Permission sets now (safely) too,
Per-backend policies dance with grace,
Terraform access finds its place! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main change: adding support for multiple Terraform state backends, which is confirmed by both the file-level summaries and PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/multiple-state-backends-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mergify mergify bot requested review from a team February 4, 2026 20:23
@mergify mergify bot added the triage Needs triage label Feb 4, 2026
@mergify mergify bot added the needs-test Needs testing label Feb 4, 2026
Copy link
Contributor

@milldr milldr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we dont need to duplicate the policy for each allowed bucket. We can just add the list of allowed resources to each existing policy. Permissions are identitical

ie

# In locals, consolidate the bucket ARNs
locals {
  # Combine the primary bucket with any additional backend buckets
  tf_access_bucket_arns = compact(concat(
    [var.tf_access_bucket_arn],
    [for k, v in var.tf_access_additional_backends : v.bucket_arn]
  ))
  
  # Similarly for DynamoDB tables (filtering out empty/null values)
  tf_access_dynamodb_table_arns = compact(concat(
    [var.tf_access_dynamodb_table_arn],
    [for k, v in var.tf_access_additional_backends : try(v.dynamodb_table_arn, "")]
  ))
  
  # And for role ARNs
  tf_access_role_arns = compact(concat(
    [var.tf_access_role_arn],
    [for k, v in var.tf_access_additional_backends : v.role_arn]
  ))
}
# In the existing S3 policy statements, change from:
resources = [var.tf_access_bucket_arn]

# To:
resources = local.tf_access_bucket_arns

# And for object-level permissions:
resources = [for arn in local.tf_access_bucket_arns : "${arn}/*"]

@Benbentwo
Copy link
Contributor Author

we dont need to duplicate the policy for each allowed bucket. We can just add the list of allowed resources to each existing policy. Permissions are identitical

ie

# In locals, consolidate the bucket ARNs
locals {
  # Combine the primary bucket with any additional backend buckets
  tf_access_bucket_arns = compact(concat(
    [var.tf_access_bucket_arn],
    [for k, v in var.tf_access_additional_backends : v.bucket_arn]
  ))
  
  # Similarly for DynamoDB tables (filtering out empty/null values)
  tf_access_dynamodb_table_arns = compact(concat(
    [var.tf_access_dynamodb_table_arn],
    [for k, v in var.tf_access_additional_backends : try(v.dynamodb_table_arn, "")]
  ))
  
  # And for role ARNs
  tf_access_role_arns = compact(concat(
    [var.tf_access_role_arn],
    [for k, v in var.tf_access_additional_backends : v.role_arn]
  ))
}
# In the existing S3 policy statements, change from:
resources = [var.tf_access_bucket_arn]

# To:
resources = local.tf_access_bucket_arns

# And for object-level permissions:
resources = [for arn in local.tf_access_bucket_arns : "${arn}/*"]

I think we do want individual policies because then you can trust certain policies to certain groups for particular buckets. This allows much more fine-grained access controls.

@mergify mergify bot removed the triage Needs triage label Feb 5, 2026
@Benbentwo Benbentwo added this pull request to the merge queue Feb 9, 2026
github-merge-queue bot pushed a commit that referenced this pull request Feb 9, 2026
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>
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Feb 9, 2026
@Benbentwo Benbentwo added this pull request to the merge queue Feb 9, 2026
Merged via the queue into main with commit d5aefb3 Feb 9, 2026
15 checks passed
@Benbentwo Benbentwo deleted the feature/multiple-state-backends-support branch February 9, 2026 17:19
@github-actions
Copy link

github-actions bot commented Feb 9, 2026

These changes were released in v2.0.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-test Needs testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants