When working on a Terraform project, it's common to save the Terraform state file in the same AWS account where resources are deployed. However, there may be scenarios where you need to maintain the state file in a separate AWS account for better security and access control. This blog post walks you through setting up such a configuration and updating your CI/CD pipeline accordingly.
Initial Setup: Terraform Project and CI/CD Pipeline
Imagine you have a Terraform project that deploys AWS resources and saves the Terraform state file in the same AWS account. You also have a CI/CD pipeline using GitHub Actions that looks something like this:
name: Deploy Terraform Infrastructure
on:
push:
branches:
- branch
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.8.5
- name: Configure AWS Credentials
id: aws-creds
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: <your role>
aws-region: <your region>
- name: Initialize Terraform
run: terraform init -backend-config="region=region" -reconfigure -backend-config="bucket=bucket-name"
- name: Plan Terraform
run: terraform plan -var="your variables" -out tf.plan
- name: Apply Terraform
if: github.event_name == 'push'
run: terraform apply "tf.plan"
Request: Storing State File in a Separate AWS Account
There may be a request to store the state file in a separate AWS account due to security reasons, such as limited access, read-only permissions, and more restrictive policies. Here's how to achieve this.
One-Time Setup: Creating Resources in the New AWS Account
First, you need to create the required resources in the new AWS account using Terraform. This is separate from the Terraform code used to deploy your application infrastructure. The Terraform template for setting up the new account might look like this:
resource "aws_iam_openid_connect_provider" "github" {
url = "https://${var.github_idp_domain}"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [var.github_idp_thumbprint]
}
provider "aws" {
region = var.region
profile = var.aws_profile
}
# Create S3 bucket
resource "aws_s3_bucket" "my_bucket" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_public_access_block" "public_access_block" {
bucket = aws_s3_bucket.my_bucket.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Create IAM role
resource "aws_iam_role" "github_actions_role" {
name = "github-actions-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Principal = {
Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.github_idp_domain}"
},
Action = "sts:AssumeRoleWithWebIdentity",
Condition = {
StringEquals = {
"${var.github_idp_domain}:aud" : "sts.amazonaws.com",
"${var.github_idp_domain}:sub" : "repo:Organisation/Repo:ref:refs/heads/Branch"
}
}
}
]
})
}
# Create IAM policy
resource "aws_iam_role_policy" "s3_upload_policy" {
name = "s3-upload-policy"
role = aws_iam_role.github_actions_role.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"s3:PutObject",
"s3:GetObject"
],
Resource = [
aws_s3_bucket.my_bucket.arn,
"${aws_s3_bucket.my_bucket.arn}/*"
]
}
]
})
}
data "aws_caller_identity" "current" {}
Migrating the State File
To migrate the state file to the new AWS account, follow these steps:
-
Pull the State File: Download your current state file.
terraform state pull > terraform.tfstate
-
Delete the .terraform Directory: Remove the initialized Terraform state to force reinitialization.
rm -rf .terraform
-
Reinitialize Terraform: Reinitialize Terraform with the new backend configuration.
terraform init -backend-config="region=<region>" -backend-config="bucket=<new bucket>" -backend-config="profile=<new aws account>"
Updating the CI/CD Pipeline
To use the profile functionality in the CI/CD pipeline, you can use the mcblair/configure-aws-profile-action@v1.0.0
action, which supports profiles. Here’s how the updated CI/CD pipeline looks:
name: Deploy Terraform Infrastructure
on:
push:
branches:
- branch
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Terraform
uses: hashicorp/setup-terraform@v1
with:
terraform_version: 1.8.5
- name: Configure AWS Credentials for Deployment
id: dev-aws-creds
uses: mcblair/configure-aws-profile-action@v1.0.0
with:
role-arn: <current-aws-account-to-deploy-aws-resources-role-arn>
aws-region: <region>
profile-name: old-aws-account
- name: Configure AWS Credentials for State File
id: infra-aws-creds
uses: mcblair/configure-aws-profile-action@v1.0.0
with:
role-arn: <new-aws-account-created-for-github-actions-to-save-tf-statefile-role-arn>
region: <region>
profile-name: new-aws-account
- name: Initialize Terraform
run: |
cd mist-deployment
terraform init -backend-config="region=<region>" -backend-config="bucket=<new bucket>" -backend-config="profile=new-aws-account" -reconfigure
- name: Plan Terraform
run: |
cd mist-deployment
terraform plan -var="profile=old-aws-account" -out tf.plan
- name: Apply Terraform
if: github.event_name == 'push'
run: |
cd mist-deployment
terraform apply "tf.plan"
Conclusion
By following these steps, you can successfully migrate your Terraform state file to a separate AWS account while maintaining a smooth CI/CD pipeline. This approach enhances security by limiting access to the state file and allows for better management and control of your AWS resources.
Top comments (1)
Great write-up, we have a bunch of articles on Github Actions in our Newsletter, check it out - packagemain.tech/p/github-actions-...