Managing Terraform State Across AWS Accounts Using GitHub Actions
Tags: terraform, aws, githubactions, cicd
Published: 2024-06-28 13:24:00

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:

```yaml
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:

```hcl
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:

1. **Pull the State File**: Download your current state file.
    ```sh
    terraform state pull > terraform.tfstate
    ```

2. **Delete the .terraform Directory**: Remove the initialized Terraform state to force reinitialization.
    ```sh
    rm -rf .terraform
    ```

3. **Reinitialize Terraform**: Reinitialize Terraform with the new backend configuration.
    ```sh
    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](https://github.com/marketplace/actions/configure-aws-profile), which supports profiles. Here’s how the updated CI/CD pipeline looks:

```yaml
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.