When working with Terraform in multi-account AWS environments, it’s often necessary to store the Terraform state in one AWS account (let’s call it Account A) while provisioning infrastructure in another account (Account B). This setup enhances security, centralizes state management, and facilitates better separation of duties. Here’s how you can achieve this using cross-account roles in AWS.
Why Separate State and Infrastructure?
Storing Terraform state files in a different AWS account than where the infrastructure is provisioned offers several benefits:
- Centralized State Management: By centralizing Terraform state in one account, you can manage and monitor state files more effectively.
- Enhanced Security: By segregating roles and permissions, you limit the exposure of sensitive state data.
- Separation of Duties: This setup supports organizations that follow strict security practices by separating administrative duties across accounts.
Step-by-Step Setup
1. Create an S3 Bucket in Account A
In Account A, create an S3 bucket to store the Terraform state files and a DynamoDB table to handle state locking. This ensures consistency and prevents concurrent operations from corrupting the state.
provider "aws" {
alias = "account_a"
region = "us-west-2"
}
resource "aws_s3_bucket" "terraform_state" {
provider = aws.account_a
bucket = "my-terraform-state-bucket"
versioning {
enabled = true
}
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
provider = aws.account_a
name = "terraform-state-locks"
hash_key = "LockID"
billing_mode = "PAY_PER_REQUEST"
attribute {
name = "LockID"
type = "S"
}
}
2. Configure the Backend to Use the S3 Bucket
In your Terraform configuration, specify the backend as the S3 bucket in Account A. Use the role_arn
to assume a role in Account B when applying infrastructure changes.
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "path/to/my/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-state-locks"
role_arn = "arn:aws:iam::ACCOUNT_B_ID:role/AccountB-TerraformRole"
}
}
3. Create a Cross-Account Role in Account B
In Account B, create an IAM role that allows Account A to assume it. This role should have the necessary permissions to provision resources.
resource "aws_iam_role" "terraform" {
name = "AccountB-TerraformRole"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
"AWS" = "arn:aws:iam::ACCOUNT_A_ID:user/your-username"
},
Action = "sts:AssumeRole"
}
]
})
}
resource "aws_iam_policy" "terraform_policy" {
name = "terraform-full-access"
description = "Full access for Terraform"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = [
"ec2:*",
"s3:*",
"iam:*",
"dynamodb:*",
"rds:*"
],
Effect = "Allow",
Resource = "*"
}
]
})
}
resource "aws_iam_role_policy_attachment" "attach" {
role = aws_iam_role.terraform.name
policy_arn = aws_iam_policy.terraform_policy.arn
}
4. Use the Assume Role in Terraform
In Account A, configure Terraform to assume the role in Account B when applying infrastructure.
provider "aws" {
alias = "account_b"
region = "us-west-2"
assume_role {
role_arn = "arn:aws:iam::ACCOUNT_B_ID:role/AccountB-TerraformRole"
}
}
Conclusion
By configuring cross-account roles and using Terraform’s backend configuration, you can securely manage infrastructure across multiple AWS accounts while keeping the state files centralized. This setup not only improves security but also aligns with best practices for managing infrastructure as code at scale.