TerraformのState管理を別のAWSアカウントのS3バケットに移行してみた

TerraformのState管理を別のAWSアカウントのS3バケットに移行してみた

Clock Icon2025.05.05

さがらです。

TerraformのState管理を別のAWSアカウントのS3バケットに移行してみたので、その内容をまとめてみます。

注意事項

本記事では、「移行元のAWSアカウントのS3バケットからローカルにstateをterraform init -migrate-stateで一度退避した後に、移行先のAWSアカウントのS3バケットに対してterraform init -migrate-stateでローカルに一時退避したstateを移行」という流れで行っています。

もっと安全に行うならば、「移行元AWSアカウント・移行先AWSアカウントそれぞれに対して必要な権限を持つIAMロールと信頼関係を設定して、移行元のAWSアカウントのS3バケットから移行先のAWSアカウントのS3バケットに対してterraform init -migrate-stateでstateを移行」という方法もあると思います。(私自身のAWS経験不足もあり、一通り移行を終えた後にこの方法が頭に浮かびました…)

そのため、この記事は「ローカルにstateを一時退避する場合の移行の手順」の参考としてご覧頂けると幸いです。

前提条件

Snowflakeのリソース管理用のTerraformのリポジトリで、Stateを移行していきます。

  • 環境分離
    • environmentsフォルダでdevフォルダとprodフォルダに分けている
    • GitHub Actionsを用いたCI/CDも実装済(こちらの記事で行った内容がベース)
  • Terraform用のS3バケットやGitHub Actionsから認証させるためのOpenID Connectのプロバイダの管理
    • CloudFormationで定義(こちらの記事で行った内容がベース)
    • 各リソース名は、移行元・移行先のAWSアカウントどちらでも同じとする
  • AWS CLIの認証情報管理
    • aws-vaultを使用
    • 本記事では、移行元・移行先のprofileについて以下のように記載する
      • 移行元のprofile名:migration-source
      • 移行先のprofile名:migration-target
  • 追加で実施すること
    • stateファイルのロックをS3だけで行うようにするため、DynamoDBのテーブルは移行先のAWSアカウントでは作成しない
  • 使用するTerraformのバージョン
    • v1.11.4

移行先のS3バケット・OpenID Connectのプロバイダ・IAMロールの作成

CloudFormationで、移行先のS3バケット・OpenID Connectのプロバイダ・IAMロールの作成をしていきます。

  • CloudFormationのコード
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Setup for GitHub Actions with OIDC, including S3 and IAM Role'

Parameters:
  S3BucketName:
    Type: String
    Default: terraform-state-bucket
    Description: Bucket name for state file
  GitHubAccountName:
    Type: String
    Default: youraccountname
    Description: GitHub account name of the repository
  GitHubRemoteRepoName:
    Type: String
    Default: youreponame
    Description: Repository name of terraform
  PolicyName:
    Type: String
    Default: GitHubActionsPolicy
    Description: Policy for GitHub Actions

Resources:
  # S3 Bucket for Terraform state
  TerraformStateBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName
      VersioningConfiguration:
        Status: Enabled

  # OIDC Provider for GitHub Actions
  GitHubOIDCProvider:
    Type: AWS::IAM::OIDCProvider
    Properties:
      Url: 'https://token.actions.githubusercontent.com'
      ClientIdList:
        - 'sts.amazonaws.com'
      ThumbprintList:
        - '6938fd4d98bab03faadb97b34396831e3780aea1'

  # IAM Role for GitHub Actions
  GitHubActionsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Federated: !GetAtt GitHubOIDCProvider.Arn
            Action: 'sts:AssumeRoleWithWebIdentity'
            Condition:
              StringLike:
                token.actions.githubusercontent.com:sub: !Sub "repo:${GitHubAccountName}/${GitHubRemoteRepoName}:*"
      Policies:
        - PolicyName: !Ref PolicyName
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # state関係の権限(S3のみ)
              - Effect: Allow
                Action: 
                  - 's3:ListBucket'
                  - 's3:GetBucketLocation'
                  - 's3:ListBucketMultipartUploads'
                  - 's3:ListBucketVersions'
                Resource: 
                  - !Sub 'arn:aws:s3:::${TerraformStateBucket}'
              - Effect: Allow
                Action: 
                  - 's3:GetObject'
                  - 's3:PutObject'
                  - 's3:DeleteObject'
                Resource: 
                  - !Sub 'arn:aws:s3:::${TerraformStateBucket}/*'
              # IAMロール作成の権限(任意:Snowflake関連でIAM管理が必要な場合があるため)
              - Effect: Allow
                Action:
                  - 'iam:CreateRole'
                  - 'iam:DeleteRole'
                  - 'iam:GetRole'
                  - 'iam:PutRolePolicy'
                  - 'iam:DeleteRolePolicy'
                  - 'iam:AttachRolePolicy'
                  - 'iam:DetachRolePolicy'
                  - 'iam:UpdateRole'
                  - 'iam:UpdateRoleDescription'
                  - 'iam:TagRole'
                  - 'iam:UntagRole'
                  - 'iam:ListRolePolicies'
                  - 'iam:ListAttachedRolePolicies'
                Resource: 'arn:aws:iam::*:role/*'
              # IAMポリシー作成の権限(任意:Snowflake関連でIAM管理が必要な場合があるため)
              - Effect: Allow
                Action:
                  - 'iam:CreatePolicy'
                  - 'iam:DeletePolicy'
                  - 'iam:GetPolicy'
                  - 'iam:GetPolicyVersion'
                  - 'iam:ListPolicyVersions'
                  - 'iam:CreatePolicyVersion'
                  - 'iam:DeletePolicyVersion'
                  - 'iam:SetDefaultPolicyVersion'
                Resource: 'arn:aws:iam::*:policy/*'
              # S3イベント通知設定の権限(任意:Snowflake関連でS3イベント通知設定をする場合があるため)
              - Effect: Allow
                Action:
                  - 's3:PutBucketNotification'
                  - 's3:GetBucketNotification'
                  - 's3:PutBucketNotificationConfiguration'
                  - 's3:GetBucketNotificationConfiguration'
                Resource: 'arn:aws:s3:::*'

Outputs:
  TerraformStateBucketName:
    Description: "Terraform state bucket name"
    Value: !Ref TerraformStateBucket

  GitHubActionsRoleArn:
    Description: "IAM Role ARN for GitHub Actions"
    Value: !GetAtt GitHubActionsRole.Arn

  GitHubOIDCProviderArn:
    Description: "OIDC Provider ARN"
    Value: !GetAtt GitHubOIDCProvider.Arn
  • 参考:CLIからのデプロイコマンド
aws-vault exec migration-target -- \
  aws cloudformation deploy \
    --template-file cfn/create_state_gha_resources.yaml \
    --stack-name terraform-state-cicd-resources \
    --capabilities CAPABILITY_NAMED_IAM

dev環境の移行

まず、dev環境の移行から行っていきます。移行作業中に他の方がTerraformの変更をかけようとするとstateの内容がズレてしまうので、注意しましょう。

backend.tfを変更し、一時的にローカルでstate管理するように設定

devフォルダのbackend.tfを、下記のように変更します。先の工程を見越して2つのbackendを記載して、片方をコメントアウトしながら進めていきます。

S3についてはuse_lockfile = trueとすることで、S3だけでstateロックが行えるようになります。

terraform {
  # 移行先のS3バケット
  # backend "s3" {
  #   bucket         = "terraform-state-bucket-sagara"
  #   key            = "snowflake-dev/snowflake-dev.tfstate"
  #   region         = "ap-northeast-1"
  #   use_lockfile   = true
  #   encrypt        = true
  # }

  # 一時退避用のローカル
  backend "local" {
     path   = "snowflake-dev.tfstate"
   }

}

移行元のprofileを使ってterraform init -migrate-stateの実行

devフォルダにて以下のコマンドを実行します。

aws-vault exec migration-source -- terraform init -migrate-state

すると下図のように移行元のS3バケットにあるstateをローカルにコピーしてそれを新しいローカルのstateとしてよいか確認がされますので、yesにしてローカルにstateをコピーします。

2025-05-05_06h19_49

backend.tfを変更し、移行先のAWSアカウントのS3でstate管理するように設定

devフォルダのbackend.tfを、下記のように移行先のAWSアカウントのS3バケットでstate管理するように変更します。

terraform {
  # 移行先のS3バケット
  backend "s3" {
    bucket         = "terraform-state-bucket-sagara"
    key            = "snowflake-dev/snowflake-dev.tfstate"
    region         = "ap-northeast-1"
    use_lockfile   = true
    encrypt        = true
  }

  # 一時退避用のローカル
  # backend "local" {
  #    path   = "snowflake-dev.tfstate"
  #  }

}

移行先のprofileを使ってterraform init -migrate-stateの実行

devフォルダにて以下のコマンドを実行します。

aws-vault exec migration-target -- terraform init -migrate-state

すると下図のように、ローカルにあるstateを移行先のS3バケットにコピーしてそれを新しいstateとしてよいか確認がされますので、yesにして移行先のS3にコピーします。

2025-05-05_06h35_37

この後S3バケットを見ると、指定したフォルダとstateファイルが作られていることがわかります。

2025-05-05_06h36_16

動作確認

念の為、terraform planを実行して差分がないことを確認しておきます。

aws-vault exec migration-target -- terraform plan

下図のように表示されたので、問題なさそうです。

2025-05-05_06h37_56

prod環境の移行

次にprod環境の移行を行っていきます。移行作業中に他の方がTerraformの変更をかけようとするとstateの内容がズレてしまうので、注意しましょう。

GitHub Actions向けのIAMロールのARNの変更以外は、dev環境で行った時と同じ内容になります。

backend.tfを変更し、一時的にローカルでstate管理するように設定

prodフォルダのbackend.tfを、下記のように変更します。先の工程を見越して2つのbackendを記載して、片方をコメントアウトしながら進めていきます。

terraform {
  # 移行先のS3バケット
  # backend "s3" {
  #   bucket         = "terraform-state-bucket-sagara"
  #   key            = "snowflake-prod/snowflake-prod.tfstate"
  #   region         = "ap-northeast-1"
  #   use_lockfile   = true
  #   encrypt        = true
  # }

  # 一時退避用のローカル
  backend "local" {
     path   = "snowflake-prod.tfstate"
   }

}

移行元のprofileを使ってterraform init -migrate-stateの実行

prodフォルダにて以下のコマンドを実行します。

aws-vault exec migration-source -- terraform init -migrate-state

すると下図のように移行元のS3バケットにあるstateをローカルにコピーしてそれを新しいローカルのstateとしてよいか確認がされますので、yesにしてローカルにstateをコピーします。

2025-05-05_06h19_49

backend.tfを変更し、移行先のAWSアカウントのS3でstate管理するように設定

devフォルダのbackend.tfを、下記のように移行先のAWSアカウントのS3バケットでstate管理するように変更します。

terraform {
  # 移行先のS3バケット
  backend "s3" {
    bucket         = "terraform-state-bucket-sagara"
    key            = "snowflake-prod/snowflake-prod.tfstate"
    region         = "ap-northeast-1"
    use_lockfile   = true
    encrypt        = true
  }

  # 一時退避用のローカル
  # backend "local" {
  #    path   = "snowflake-dev.tfstate"
  #  }

}

移行先のprofileを使ってterraform init -migrate-stateの実行

prodフォルダにて以下のコマンドを実行します。

aws-vault exec migration-target -- terraform init -migrate-state

すると下図のように、ローカルにあるstateを移行先のS3バケットにコピーしてそれを新しいstateとしてよいか確認がされますので、yesにして移行先のS3にコピーします。

2025-05-05_06h35_37

この後S3バケットを見ると、指定したフォルダとstateファイルが作られていることがわかります。

2025-05-05_06h51_00

動作確認

念の為、terraform planを実行して差分がないことを確認しておきます。

aws-vault exec migration-target -- terraform plan

下図のように表示されたので、問題なさそうです。

2025-05-05_06h51_38

GitHub Actions用にIAMロールのARNを登録

GitHub ActionsでCI/CDを実行しているため、GitHub Actions用にIAMロールのARNを登録します。

CloudFormationで作成したIAMロールのARNを確認します。

2025-05-05_06h54_46

その上で、GitHubで対象のリポジトリのSecrets and variablesの画面で、元々IAMロールのARNを設定していたsecretを上書きします。

2025-05-05_06h58_02

動作確認

今回backend.tfなどのコードを変更していたので、その変更をGitHubに反映させるためにプルリクエストを発行したついでに動作確認を行います。

下図のように、プルリクエスト発行時のterraform plan、マージ後のterraform applyどちらも問題なく動いたことがわかります。

2025-05-05_07h04_16

2025-05-05_07h41_44

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.