Terraform WorkspacesとAssume Roleで複数AWSアカウントに同じAWSアーキテクチャをプロビジョニングする

2021.03.30

やりたいこと

シングルテナントで提供するサービスを構想しています。つまり複数の(しかも結構な数の)AWSアカウントに同じAWSの構成をプロビジョニングする必要があります。これをTerraformで実現したいです。Stateファイルは各テナント(アカウント)毎に分けたいです。

実装方針

Terraform workspacesとIAMのAssume Roleを使って実現します。
テナントごとにworkspaceを作成し、aws providerのcredentialの設定をAssume Roleにし、workspace毎にAssume Role先を切り替えます。Assume Role元は、このプロビジョニング専用のアカウント内のIAMエンティティ(IAM RoleかIAM User)です。各テナントのStateファイルはプロビジョニング専用のアカウント内のS3バケット内に保存されます。

workspace-assumeole-architecture-2

実装

※今回はサンプルなので、各アカウントのProvisioned ResourcesはS3バケット一つだけとします。

Assume Role元のIAM Roleを作成

Provisioning Accountにて、プロビジョニング先の各アカウントのロールにAssume RoleするIAM Roleterraform-roleを作成します。

アタッチするポリシーは以下です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": "arn:aws:iam::*:role/multiple-provision-test-role"
        },
        {
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::kazue-s3-backend"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject"
            ],
            "Resource": [
                "arn:aws:s3:::kazue-s3-backend/multiple-provision-test.tfstate",
                "arn:aws:s3:::kazue-s3-backend/env:/*/multiple-provision-test.tfstate"
            ]
        }
    ]
}

1つ目のAllowステートメントが各アカウントのIAM RoleにAssume Roleするための権限です。Resource句のARNの、AWSアカウントIDを入れる部分を*にしているのがポイントです。残り2のステートメントはS3 backendへアクセスするための権限です。

プロビジョニング先アカウントにIAM Roleを作成

プロビジョニング先の各アカウントにAssume Role先のIAM Roleを作成します。IAM Role名はmultiple-provision-test-roleとすべて同じにします。

信頼Policy

Principal句で、先程作ったProvisioning Account内のIAM RoleからのみAssume Roleを許可します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/terraform-role"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

IAM Policy(アクセス権限)

AWS 管理ポリシーのAdministratorAccessをアタッチします。

Terraformコードを作成

S3バケットを一つ作成するコードです。

terraform {
  required_version = "= 0.14.7"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.29.1"
    }
  }

  backend "s3" {
    bucket = "kazue-s3-backend"
    key    = "multiple-provision-test.tfstate"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  region = "ap-northeast-1"
  assume_role {
    role_arn     = "arn:aws:iam::${terraform.workspace}:role/multiple-provision-test-role"
    session_name = "terraform"
  }
}

resource "aws_s3_bucket" "test" {
  bucket_prefix = "${terraform.workspace}-kazue-test-"
}

ポイントはAWS Providerの assume_roleブロックのrole_arnです。アカウントID指定部分を${terraform.workspace}と動的にしています。

backend用S3バケットも予め作成しておきましょう。

Workspace作成

事前に先のProvisioning Accountのロールを引き受けている状態にしておきます。

$ aws sts get-caller-identity
{
    "UserId": "XXXXXXXXXXXXXXXXXXXXX:kazue",
    "Account": "0123456789012",
    "Arn": "arn:aws:sts::0123456789012:assumed-role/terraform-role/kazue"
}

terraform init

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v3.29.1...
- Installed hashicorp/aws v3.29.1 (signed by HashiCorp)

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

terraform workspace new

$ terraform workspace new 111111111111
Created and switched to workspace "111111111111"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

プロビジョニング

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.test will be created
  + resource "aws_s3_bucket" "test" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = (known after apply)
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = "111111111111-kazue-test-"
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + versioning {
          + enabled    = (known after apply)
          + mfa_delete = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions in workspace "111111111111"?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_s3_bucket.test: Creating...
aws_s3_bucket.test: Creation complete after 3s [id=111111111111-kazue-test-20210330111653262000000001]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

S3バケットが作成されました!

別のアカウントにプロビジョニング

今回はterraform initは不要です。

$ terraform workspace new 222222222222
Created and switched to workspace "222222222222"!

You're now on a new, empty workspace. Workspaces isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_s3_bucket.test will be created
  + resource "aws_s3_bucket" "test" {
      + acceleration_status         = (known after apply)
      + acl                         = "private"
      + arn                         = (known after apply)
      + bucket                      = (known after apply)
      + bucket_domain_name          = (known after apply)
      + bucket_prefix               = "222222222222-kazue-test-"
      + bucket_regional_domain_name = (known after apply)
      + force_destroy               = false
      + hosted_zone_id              = (known after apply)
      + id                          = (known after apply)
      + region                      = (known after apply)
      + request_payer               = (known after apply)
      + website_domain              = (known after apply)
      + website_endpoint            = (known after apply)

      + versioning {
          + enabled    = (known after apply)
          + mfa_delete = (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions in workspace "222222222222"?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_s3_bucket.test: Creating...
aws_s3_bucket.test: Creation complete after 3s [id=222222222222-kazue-test-20210330112053303300000001]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Stateファイルの構成は以下のようになります。

aws s3 ls s3://kazue-s3-backend --recursive --human-readable
2021-03-30 20:16:57    1.8 KiB env:/111111111111/multiple-provision-test.tfstate
2021-03-30 20:20:57    1.8 KiB env:/222222222222/multiple-provision-test.tfstate

まとめ

Terraform WorkspacesとAssume Roleを使って、複数のAWSアカウントに同じ構成をプロビジョニングする方法をご紹介しました。今後はAssume Role元のRoleをCodeBuildに引き受けさせて、プロビジョニングの自動化を検討していきます。

参考情報