AWS - GCP の ID 連携を使い、 AWS CodeBuild で Terraform を使って GCP を管理

こんにちは。 SRE の @suzuki-shunsuke です。

Google Cloud Platform (以下 GCP) を Terraform で管理するように CI/CD を整備した話を紹介します。

背景

何度か本ブログで紹介したように、弊社では Terraform を使い AWS を始めとする様々なリソースを管理しています。

quipper.hatenablog.com

しかし、 GCP はあまりちゃんと管理できていないという課題がありました。

弊社のサービスはほぼすべて AWS 上で動いており、 IaC が既に整備されています。 一方で GCP も以前から使っていますが IaC は整備されてなく、 SRE がたまに developer (以下 dev) から依頼を受けて、手で Project を作ったり IAM 周りを設定したりしていました。 IaC ができていないため、 手で設定できるように dev に強めの権限を付与することも少なくありませんでした。

以前から Terraform で管理したいと思いつつ、他のタスクとの優先度の兼ね合いで後回しにしてきましたが、 最近自分が依頼に複数回対応したのをきっかけに Terraform の CI/CD を整備することにしました。

余談ですが、 GCP を Terraform で管理する動きがこれまで全く無かったわけではなく、 GCP を主に使っているチームではローカルで Terraform を apply するようなこともしていました。 しかし CI/CD を整備するまでには至っていないようでした。

どうやって CI/CD で Terraform を実行するか

当初 Google Cloud Build (以下 Cloud Build) を使うことを検討していましたが、幾つか解決するのが難しい課題があり、 AWS CodeBuild (以下 CodeBuild) 上で AWS - GCP の ID 連携を行って Terraform を実行することにしました。

なぜ Cloud Build を最初に検討したか

GCP 上で Terraform を実行すると、 CircleCI や AWS のような GCP 外部のサービスに GCP の Service Account (以下 SA) の key を登録する必要がなく、 key が流出するリスクがないからです。 これは弊社が AWS を Terraform で管理するために CodeBuild を使っているのと同じ理由です。

Cloud Build の課題

弊社では AWS などを Terraform で管理するための CI/CD が CodeBuild を使って実現されており、それと同様のことを Cloud Build で実現する方法を検討しました。 前提として Monorepo で複数の GCP Project を管理する必要があります。

少し考えただけでも、以下のようなことを考慮する必要がありました。

また、上記とは別に大きな課題が見つかりました。 Cloud Build で使う SA を Trigger ごとに変更できないという問題です。

cloud.google.com

デフォルトでは、Cloud Build は特別なサービス アカウントを使用してユーザーの代わりにビルドを実行します

ユーザー指定のサービス アカウントは手動ビルドでのみ機能します。ビルドトリガーでは機能しません

これだと、 Terraform を実行するために Cloud Build の SA に強めの権限を付与すると、同じ Project の別の Trigger にもその権限が付与されてしまいます。

Trigger で起動した build 内で gcloud builds submit で SA を指定して Terraform を実行するという手も検討しましたが、 それだと gcloud builds submit で起動した build は PR とは関連づかず、 commit status が更新されません。 commit status を GitHub API で更新することもできますが、そんなことまでしたくありません。

Cloud Build を使うために考えられる選択肢は他にもありますが、それよりは後述する CodeBuild を使ったアプローチのほうが弊社にはあっていると思いました。

GCP Workload Identity Federation

GCP Workload Identity Federation (以下 ID連携) は、 GCPAWS などの他の ID プロバイダを連携し、 Service Account の key を使わずに GCP のリソースにアクセスできるようにする仕組みです。 AWS の場合 AWS の IAM Role や User を使い、 GCP の Service Account として GCP にアクセスできるようになります。

なぜ CodeBuild になったのか

ID 連携を使えば CodeBuild から SA の key なしで GCP にアクセスできます。 加えて弊社では以下のようなメリットがありました。

  • CI のコードや仕組みを既存のものとほぼ共通化できる
    • すでに CodeBuild で実装されている仕組みを Cloud Build に移植するのは難しいし、できたとしても手間だし、メンテも大変
  • Backend の Bucket も共通化できる(Google Provider 使いつつ、 Backend は S3)
    • GCP の Project ごとに bucket 作ったりする必要がない
  • GitHub Access Token のような CI に必要な Secret を GCP (Secret Manager) に登録する必要がない

ID 連携を使えば既存の仕組みを流用し、最小限の変更・作業で GCP を Terraform で管理できるようになるため、これがベストだと感じました。

ID 連携 on CodeBuild

AWS - GCP で ID 連携する方法は公式ドキュメントに書いてあります。

cloud.google.com

Project ごとに Terraform を実行するための Service Account terraform を作っています。 Provider の Attribute Conditions でCodeBuild の Service Role だけが ID 連携できるように制限しています。

attribute.aws_role == "arn:aws:sts::<AWS Account ID>:assumed-role/<Service Role name>"

しかし公式の手順通りだと認証情報の自動生成がうまく行かず、 ググってもあまり情報がなく、まる一日くらいハマりました。

以下に自分が解決した方法を書きますが、より適切な解決方法があれば教えてもらえると助かります。

まずダウンロードした構成ファイルから credential_source の url, region_url を消します。

AS IS

  "credential_source": {
    "environment_id": "aws1",
    "region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
    "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
    "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
  }

TO BE

  "credential_source": {
    "environment_id": "aws1",
    "regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"
  }

そして CI で http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI にアクセスし、認証情報を環境変数に設定するようにしました。

# Don't set -x to prevent access key from being outputted.
set -eu
set -o pipefail

token_file=$(mktemp)
# https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-iam-roles.html
curl -sS "http://169.254.170.2$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI" > "$token_file"
AWS_ACCESS_KEY_ID=$(jq -r ".AccessKeyId" "$token_file")
AWS_SECRET_ACCESS_KEY=$(jq -r ".SecretAccessKey" "$token_file")
AWS_SESSION_TOKEN=$(jq -r ".Token" "$token_file")
rm "$token_file"

export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export AWS_SESSION_TOKEN

構成ファイルは credential ではないので、 terraform を実行するリポジトリにコミットし、 CI で構成ファイルへのパスを環境変数 GOOGLE_APPLICATION_CREDENTIALS として設定します。

なぜ修正が必要か

なぜ上記のような修正が必要かというと、ダウンロードした構成ファイルは EC2 instance 上で ID 連携することを想定しており、 CodeBuild 上で ID 連携することは想定していないのだと思います。

公式の手順そのままだと terraform plan 実行時に次のようなエラーが発生しました。

Get "http://169.254.169.254/latest/meta-data/iam/security-credentials": dial tcp 169.254.169.254:80: i/o timeout

この URL は構成ファイルの credential_sourceurl であり、 http://169.254.169.254/latest/meta-data/ は EC2 instance の meta data を取得するための endpoint です。

docs.aws.amazon.com

CodeBuild にはこのような endpoint はないので timeout が発生しています。 では CodeBuild に互換性のある endpoint が他にあるかというと、おそらくないのでどうしたらいいのか悩みましたが、 構成ファイルの regional_cred_verification_url でググってみたところ、 Node.js ではありますが Google Auth Library のドキュメントを見つけました。

https://googleapis.dev/nodejs/google-auth-library/latest/interfaces/AwsSecurityCredentials.html#source

  credential_source: {
    environment_id: string;
    // Region can also be determined from the AWS_REGION or AWS_DEFAULT_REGION
    // environment variables.
    region_url?: string;
    // The url field is used to determine the AWS security credentials.
    // This is optional since these credentials can be retrieved from the
    // AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN
    // environment variables.
    url?: string;
    regional_cred_verification_url: string;
  };

それによると、 region_url, url は環境変数で補完できることが分かったので、 上記のシェルスクリプトのように CodeBuild 上で Access Key を生成し、環境変数として設定することで解決できました。

さいごに

以上、 AWS - GCP の ID 連携を使い、 CodeBuild で Terraform を使って GCP を管理できるようにした話を紹介しました。 今後徐々に既存の GCP Project を Terraform で管理するようにしていきたいと思っています。 また、 ID 連携は非常に便利な機能なので、今回の件に限らず、これまで Service Account の key を発行していたものを ID 連携に置き換えていきたいと思っています。