ねぎ嫌い

始業前に学んだことを小出しに。最近はHacker Newsの人気記事をまとめてみたり。

BitBucket PipelineでAWS Organizationsの各アカウントに対応したterraformの構築がしたい!

背景

  • CI/CDのツールとしてBitBucket Pipelineを使う必要があった
  • AWS Organizationsにより環境ごとにAWSアカウントが分かれていた

方針

  • 可能な限り余計な情報を持ちたくないので、OIDCを用いた仕組みを構築したい
  • 手元での実行もありうるが変にロックを掛け合いたくないので、terraformのstate/lockはリモートで管理したい

まずはOIDCを組むために、AWSアカウントと何かしらを紐づけて管理する必要がある。
今回はDeploymentsの1つの設定とAWSアカウントが紐づく形をとることで解決を図る。

2つ目はterraformを実行するために必要な設定となるのでAWS CLIを用いて作成する。
これ自体は別に都度回すようなものでもないので、手作業。

実装

1. BitBucket PipelineとAWSでOIDCができるようにする

参考:
Bitbucket Pipelines OpenID Connect を使用して AWS にデプロイする | Bitbucket Cloud | Atlassian サポート

基本的には上記公式ドキュメントを参考にして設定をしてあげれば良い。
前提として、設定を行う際にはリポジトリの設定を行える権限を持っている必要がある。
これがないとリポジトリごとの"プロバイダーURL"を取得できない。

a. リポジトリのプロバイダーURLとAudience等必要な値を取得しておく

リポジトリの設定画面から、OpenID Connectの設定画面を開く。
IDプロバイダの設定に必要なIdentity provider URLAudienceの値を取得しておく。

repository setting

さらに、後続で必要となる環境ごとの識別に必要なDeployments EnvironmentのUUIDを拾っておく。
ここではTestのEnvironmentに対して20af...のUUIDが発行されている。

Uniquer identifiersのexample payloadにある通りのリクエストがAWSに送られ、AWSはそれを検証するイメージ。
なのでAWS側にどの値を許可するのかを設定してあげる必要がある。

b. IDプロバイダの設定を行う

terraformの連携先となるAWSのアカウントにログインし、IAMからIDプロバイダの設定を行う。
基本的には公式ドキュメントに書いてある通りなので引っかかりそうなポイントだけ記載する。
この画面での完成形は下図のようになる。

IDプロバイダの設定

  • プロバイダを追加する際は、OpenID Connectを選択する
  • プロバイダのURLはリポジトリごとに払い出されるURLを指定する
    • 念の為サムプリントを取得して、疎通を確認しておく
  • 対象者には、Audienceの値を入力する
    • わかりやすい名前で、と思って適当な名前入れたらIAM Roleの検証で詰まって時間溶かしたので要注意...

c. IAM Roleの作成

ここで設定したIAM Roleを使ってterraformはアクセスをする。
ので、紐づけるIAM Policyは作成したいリソースに限ったものを付与する。
ここでは面倒なのでAdministratorの権限をつけてしまうが、ちゃんとやるなら最小権限の原則に従う。

ドキュメント記載の通り、エンティティタイプはウェブアイデンティティを選択すること。
選択すると、上で作成したbitbucketのリポジトリをIDプロバイダとして選択できるようになる。
また、IDプロバイダを選択するとAudienceも選択できる。

(ここでは)ポリシーにはAdministratorAccessを選択する。

ロールの名前や説明は適当にやっておく。
信頼されたエンティティを選択するでは前述したDeploymentsを指定してそれ以外の環境からのアクセスは弾くようにしておきたい。
なので、以下のようにConditionsを変更する

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Principal": {
                "Federated": "arn:aws:iam::xxxxxxxxxxxx:oidc-provider/api.bitbucket.org/2.0/workspaces/xxxxxxxxxx/pipelines-config/identity/oidc"
            },
            "Condition": {
-                "StringEquals": {
-                    "api.bitbucket.org/2.0/workspaces/xxxxxxxxxx/pipelines-config/identity/oidc:aud": [
-                        "ari:cloud:bitbucket::workspace/2ab1d73d-ec10-4934-8233-xxxxxxxxxxxx"
-                    ]
-                }
+               "StringLike": {
+                    "api.bitbucket.org/2.0/workspaces/xxxxxxxxxx/pipelines-config/identity/oidc:sub": "{b0ef120b-014d-4b4f-96c8-xxxxxxxxxxxx}:{20af05a4-cc1b-4e45-ad22-xxxxxxxxxxxx}:*"
+                }
            }
        }
    ]
}

元々はAudienceが一致しているかを見ていたが、今回はDeploymentsまで含めて見たいのでsubjectの値を見る。
公式ドキュメントで言えば ここらへん で言及されている話題になる。

Roleの作成ができたら一旦完了。 これを環境分繰り返しておく。もちろん、環境ごとにDeployments のUUIDを変えること。

d. とりあえず動作して確認する

一旦動かして確認するのであれば以下のようなbitbucket pipelineを用意して確認するとよさそう。

image: atlassian/pipelines-awscli
pipelines:
  branches:
    test:
      - step:
          name: s3 ls on test
          deployment: test
          oidc: true
          script:
            - export AWS_REGION=ap-northeast-1
            - export AWS_ROLE_ARN=arn:aws:iam::xxxxxxxx:role/BitbucketPipelineForTesting
            - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
            - echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
            - unset AWS_SECRET_ACCESS_KEY
            - unset AWS_ACCESS_KEY_ID
            - aws s3 ls
    staging:
      - step:
          name: s3 ls on staging
          deployment: staging
          oidc: true
          script:
            - export AWS_REGION=ap-northeast-1
            - export AWS_ROLE_ARN=arn:aws:iam::yyyyyyyy:role/BitbucketPipelineForTesting
            - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
            - echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
            - unset AWS_SECRET_ACCESS_KEY
            - unset AWS_ACCESS_KEY_ID
            - aws s3 ls

重要なのは、AWS_ACCESS_KEY_IDを一度unsetしておくこと。
これがあるとどちらも同じアカウントで動作してしまう。たぶんAWS CLIの内部でAWS_ACCESS_KEY_IDが優先されているだけだと思うんだけど。
今回は AWS_WEB_IDENTITY_TOKEN_FILE が指定されているのでそちらをベースに動く。
terraformも同様の動作をする。

もしかしたら共通のステップを用意して、AWS_ROLE_ARNをdeploymentのvariableに指定すればもっと綺麗になるかもしれないがご容赦いただければと。

2. terraform用に必要なリソースを準備する

方針に記載の通り、リモートでstate/lockを管理したい。
stateはS3で、lockはdynamodbで管理するのが一般的っぽいのでそちらを。

a. S3 バケットの作成

特に複雑に考えず、ap-northeast-1にバケットを作成し、public accessをブロックし、バージョニングを有効にしているだけ。

export BUCKET_NAME=tekitouna-tfstate-dev
aws s3api create-bucket --bucket $BUCKET_NAME --region ap-northeast-1 --create-bucket-configuration LocationConstraint="ap-northeast-1"

aws s3api put-public-access-block --region ap-northeast-1 --bucket $BUCKET_NAME --public-access-block-configuration BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

aws s3api put-bucket-versioning --region ap-northeast-1 --bucket $BUCKET_NAME --versioning-configuration Status=Enabled

b. dynamodbのテーブル作成

export TABLE_NAME=tekitouna-tflock-dev
aws dynamodb create-table \
  --attribute-definitions "AttributeName=LockID,AttributeType=S"\
  --table-name "${TABLE_NAME}" \
  --key-schema  "AttributeName=LockID,KeyType=HASH" \
  --billing-mode PAY_PER_REQUEST

LockIDの指定はterraform側の仕様なのでそれに従っている
ref. Backend Type: s3 | Terraform | HashiCorp Developer

c. ローカルから接続できるか確認する

とりあえずbackend configはファイル指定の方が楽だと思うので以下のようなファイルを作って指定させる

./env/dev.tfbackend

bucket = "tekitouna-tfstate-dev"
key    = "dev.tfstate"
region = "ap-northeast-1"
dynamodb_table = "tekitouna-tflock-dev"

./provider.tf

terraform {
  backend "s3" {
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.70.0"
    }
  }
}

terraform init -reconfigure -backend-config ./env/dev.tfbackend で動くことを確認

3. BitBucket Pipelineに実装する

あとは先ほど作成したテスト用のbitbucket pipelineをいじっていい感じにすればOK

image: hashicorp/terraform:1.4
pipelines:
  branches:
    test:
      - step:
          name: terraform-plan on test
          deployment: test
          oidc: true
          script:
            - export AWS_REGION=ap-northeast-1
            - export AWS_ROLE_ARN=arn:aws:iam::xxxxxxxx:role/BitbucketPipelineForTesting
            - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
            - echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
            - unset AWS_SECRET_ACCESS_KEY
            - unset AWS_ACCESS_KEY_ID
            - terraform init -input=false -backend-config=env/dev.tfbackend -reconfigure
            - terraform plan -input=false -var-file=env/dev.tfvars
    staging:
      - step:
          name: terraform-plan on staging
          deployment: staging
          oidc: true
          script:
            - export AWS_REGION=ap-northeast-1
            - export AWS_ROLE_ARN=arn:aws:iam::yyyyyyyy:role/BitbucketPipelineForTesting
            - export AWS_WEB_IDENTITY_TOKEN_FILE=$(pwd)/web-identity-token
            - echo $BITBUCKET_STEP_OIDC_TOKEN > $(pwd)/web-identity-token
            - unset AWS_SECRET_ACCESS_KEY
            - unset AWS_ACCESS_KEY_ID
          - terraform init -input=false -backend-config=env/dev.tfbackend -reconfigure
              - terraform plan -input=false -var-file=env/dev.tfvars