本ブログでは、従来行われてきた CI 環境にAWS などのクラウドサービスのシークレットを保持する方法ではなく、OpenID Connect 認証を用いることで、GitHub などのバージョン管理システムの 1. どのプロジェクトに対して 2. 誰が変更を加えたのか をもとに、クラウドサービスの適切なロールを付与し、CI 環境からアクセスする方法をご紹介します。
ガイドの目次
CI/CD とシークレットとの関係
AWS にデプロイするサービスを開発しているとしましょう。サービスのソースコードは、みなさんの所属する組織のプロジェクトとして、GitHub 上に格納されています。組織にはメンバーが3人おり、AWS 上のプレビュー環境には誰もが(動作検証のために)デプロイできるものの、AWS 上の本番環境には、特定の一人だけがデプロイできるといった状況を想定してみましょう。
CircleCI のような CI/CD ツールが AWS のようなクラウドにサービスをデプロイする際、AWS CLI を設定する環境変数 で挙げられているような、
- AWS_ACCESS_KEY_ID: IAMユーザー または ロールに関連づけられる AWS アクセスキー
- AWS_SECRET_ACCESS_KEY: アクセスキーに関連づけられるシークレットキー
- AWS_DEFAULT_REGION: リクエストを送信する AWS リージョン
と言った環境変数を、プロジェクトでの環境変数の設定 に書かれているように、個々のプロジェクトに対して設定するか、コンテキストの生成と使用 に書かれているように、コンテキストに対して設定し、該当するコンテキスト名を プロジェクトごとに用意された CircleCI の設定ファイル (config.yml)中で指定する方法が、これまで取られてきました(この場合、プロジェクト間で環境変数を共有することが可能)。
プロジェクトごとの環境変数を使うにせよ、組織単位のコンテクストをプロジェクトに割り当てるにせよ、最終的にはプロジェクトの中で環境変数は設定されています。したがって、個々の開発メンバーに対して、AWS に対する異なる権限を割り当てるということができないのです。
CircleCI による OpenID Connect (OIDC) トークンのサポート
このような形で、組織単位であれ、プロジェクト(リポジトリ単位)であれ、CircleCI のような CI サービスの側に AWS シークレットを保持したくないというニーズがあり、そのニーズに応えることが求められてしました。
より正確には、理由1の「持ちたくない」に加え、理由2に挙げた「1通りの(往々にして強力な)権限をプロジェクトメンバー全員に一律に持たせたくない」という2つの理由を挙げることができます。
これらのニーズに応えるため、CircleCI では、ジョブに対して OpenID Connect に準拠した署名を付与する仕組み(OpenID Connect (OIDC) トークンの使用)を2022年3月にサポートしました。
プロトコルの流れ
それでは、「ジョブに対して OpenID Connect に準拠した署名を付与する仕組み」について、順を追って具体的に見ていきましょう。
信頼関係の構築 (ID Provider)
信頼関係を構築というのは、
- CircleCI 上の組織 ID(Organization ID) を元に、CircleCI ジョブが AWS に対して何か実行しようとする際に
- AWS 側から CircleCI に対して実行依頼元の組織について確認を行うための ID プロバイダ (Privider) を
AWS側に登録することを指しています。
これにより、組織(Organization)単位での信頼関係が構築されました。
JWT の付与 (Subject)
実際にジョブを実行する際に、
- 誰がジョブを実行しようとしており (User ID)
- そのジョブが属するプロジェクトは何か (Project ID)
に関しては、OpenID ConnectID トークン中の sub クレームとして格納されています。
Project ID の値は、CircleCI のプロジェクト設定画面上にあります。
また、User ID の値は、CircleCI のユーザ設定画面上にあります。
このプロジェクトには、開発メンバーとして上から順に筆者、筆者の息子、筆者の娘が所属しています。
JWT のクレーム内容に基づき認可 (Role)
このプロジェクトでは、User ID(CircleCI 側のユーザ)に基づき、
- プレビュー用の S3 に書き込み可能なロール(最終的に筆者、息子、娘に割り当て) - mshk-jekyll-preview-role
- 本番用の S3 に書き込み可能なロール(最終的に筆者に割り当て) - mshk-jekyll-deploy-role
の2つのロールを AWS 側にまずは定義しておきます。また、先ほど用意しておいたID プロバイダーを「信頼されたエンティティ」として設定しておきます。
JWT のクレーム内容に基づき認可 (信頼ポリシー)
さて、2つのロールが用意できたので、JWT のクレーム内容に基づき、ロールを割り当てるための条件(Condition) の定義を行います。
JWT のクレーム内容に基づきポリシー付与 (Policy)
また、実際の権限は、プレビュー用の S3、デプロイ用の S3 のそれぞれに対して、ListBucket, PutObject, DeleteObject で示される書き込み可能な権限を付与しており、それぞれ該当するロールにアタッチしています。
CircleCI のコンフィグ
それでは、CircleCI のコンフィグを見ていきましょう。
ワークフロー
ここでは、test-deploy というワークフローを定義しており、(Jekyll を使った)ウェブコンテンツのビルドに成功したら、
- preview で終わるブランチ名での作業であれば preview ジョブを実行し
- main ブランチでの作業であれば deploy ジョブを実行
するように定義されています。
ジョブ
次に、先に触れた preview ジョブと deploy ジョブの内容を見ていきます。
preview ジョブでは、aws-cli Orb を使ってセットアップする際に、role-arn としてプレビュー用のロール (mshk-jekyll-preview-s3) の ARN を渡しています。したがって、このジョブを実行する CircleCI ユーザーが、AWS のロール付与の条件に合致すれば(つまり、筆者、息子、娘であれば)、mshk-jekyll-preview-policy が付与され、その結果、後続のステップを実行することができます。
一方、deploy ジョブでは、role-arn としてデプロイ用のロール (mshk-jekyll-deploy-s3) の ARN を渡しています。したがって、このジョブを実行する CircleCI ユーザーが、AWS のロール付与の条件に合致すれば(つまり、筆者であれば)、mshk-jekyll-deploy-policy が付与され、その結果、後続のステップを実行することができます。
まとめ
CircleCI と AWS が OIDC 認証を使ってできることは、CircleCI 側に AWS シークレットを保持することなく、AWS に対して処理の依頼(今回の例では、S3 バケットへの書き込み)ができるということはもちろんですが、もう少し細かく見てみると、CircleCI の
- どのプロジェクトに対して (Project ID)
- 誰が (User ID)
- どのジョブを実行するのか (ジョブごとの role-arn 指定)
に応じて、AWS の
- Role を振り分け(Assume して)、その結果、
- 適用される Policy に応じて AWS サービスに対してのアクセス可否を制御できる
ということになります。
今回は、AWS を例にとって説明させていただきましたが、対象は OIDC 認証をサポートしていれば、AWS に限定されません。また、プロジェクトを格納する VCS も GitHub だけでなく、BitBucket や GitLab でも同様に設定を行うことが可能です。