こんにちは。
メディアサービス開発部バックエンド開発グループのフサギコ(髙﨑)です。
Ruby on Railsによるバックエンドの実装運用と、AWSによるサービスインフラの設計構築を中心とした、いわゆるテックリードのような立ち位置で働いています。
本記事では、GitHub ActionsとAWS IAMをOpenID Connectを使って連携させ、AWSのAPIをアクセスキーなしで操作する利点と、その仕組みをTerraformで構築する手順についてお話します。
訂正とお詫び(2022/3/22)
GitHub Actionsのワークフローを作成する
の項において、ワークフロー単位のpermissionsとジョブ単位のpermissionsの関係について正しくない記述がありましたので訂正しました。
正しくない内容を記述してしまったことについて訂正してお詫びします。
GitHub ActionsからAWSのAPIを操作する際に従来存在した課題
メディアサービス開発部では、TypeScriptで記述したアプリケーション(Next.js, Serverless Framework)のデプロイや、TerrafromのリポジトリにPRを出した際の自動planにGitHub Actionsを使用しています。
従来はGitHub ActionsからAWSのAPIを操作しようとすると、
- 必要な権限を持ったCI/CD用のIAMユーザを作成
- IAMユーザのアクセスキーを生成しダウンロード
- リポジトリのGitHub Actions用secretストアにアクセスキーを格納
という手順をCI/CDが必要な全てのリポジトリに対して行う必要がありました。
手順1はTerraformで行っていましたが2と3はエンジニアが手動で行っていたため、下記のような課題がありました。
- 一時的にでもアクセスキーを自分のコンピュータで扱わなければならないリスク
- 有効期限がないアクセスキーをGitHub Actions用secretストアに預けなければならないリスク
- そして何より単純に煩雑
これらの課題は、GitHubが去年の10月末にリリースしたGitHub ActionsのOpenID Connectプロバイダー機能によって解決できます。
OpenID Connectプロバイダー機能をAWSのIAMと組み合わせて使うと、従来リポジトリのGitHub Actions用secretストアに格納していたIAMユーザのアクセスキーの代わりに、IAMロールの権限を持った有効期限付きのアクセスキーを発行できます。
そうすることで、自分のコンピュータでアクセスキーを扱うことなく、万が一アクセスキーが漏れても一定時間で使えなくなり、新しいリポジトリのActionsにAWSの操作権限を与える際もTerraformを変更するだけという、セキュアかつ手軽な状態を実現できます。
GitHub ActionsのOpenID Connect連携をTerraformで構築する
この節では、AWSをTerraformで管理できるようになっていること、GitHub Actionsの書き方をある程度理解していることは前提として、OpenID ConnectによってGitHub ActionsがAWS APIを操作できる状態をTerraformで構築する手順について述べます。
OpenID Connect Providerを作成する
下記のようにして、OpenID Connect Provider設定をAWS IAMに作成します。
data "http" "github_actions_openid_configuration" { url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration" } data "tls_certificate" "github_actions" { url = jsondecode(data.http.github_actions_openid_configuration.body).jwks_uri } resource "aws_iam_openid_connect_provider" "github_actions_oidc_provier" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] thumbprint_list = [data.tls_certificate.github_actions.certificates[0].sha1_fingerprint] }
GitHub ActionsはAWS IAMへ一時アクセスキーの発行をリクエストする際に、GitHub Actionsが署名したJWTをtokenとして添付します。 ここで作成しているOpenID Connect Provider設定は、そのGitHub Actionsが署名したJWTをAWS IAMが検証する際に用いるものです。
上の二つのデータソースでGitHubがJWTの署名に用いる鍵の公開鍵を取得し、そのフィンガープリントをOpenID Connect Providerに設定しています。
何らかの理由によってGitHubがJWTの署名に用いる鍵が変わると、このaws_iam_openid_connect_provider.github_actions_oidc_provier
に差分が出ることになります。
その際はこのようにして公式から告知があるはずですので、必ずそれを確認してから適用するようにしましょう。
OpenID ConnectはJWTの署名とその検証によって成り立つ仕組みのため、公開鍵のフィンガープリントをしっかり確認するのはセキュリティの根幹です。もし確認を怠ると、予期しない署名者を誤って信頼してしまう可能性があるため、これはしっかりやっておく必要があります。
この記事を書いている2022/3/15現在のフィンガープリントは前述の告知から変わっておらず、6938fd4d98bab03faadb97b34396831e3780aea1
です。
IAMロールを作成する
下記のようにして、GitHub Actionsが借り受けるIAMロールを作成します。
resource "aws_iam_role" "example_role" { name = "github_actions_open_id_connect_example_role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:AssumeRoleWithWebIdentity" Effect = "Allow" Principal = { Federated = [aws_iam_openid_connect_provider.github_actions_oidc_provier.arn] } Condition = { StringLike = { "token.actions.githubusercontent.com:sub" = [ "repo:${var.org_name}/${var.repo_name}:ref:${var.ref_prefix}", ] } } }] }) } resource "aws_iam_role_policy" "example_role_policy" { name = "example_role_policy" role = aws_iam_role.example_role.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Action = "sts:GetCallerIdentity" Effect = "Allow" Resource = "*" }] }) }
IAMロールのassume_role_policyというとActionがsts:AssumeRole
で、PrincipalのServiceにec2.amazonaws.com
などAWSのサービスのドメイン名が指定されていることが多いでしょう。
一方、OpenID Connect経由で借り受けられるIAMロールのassume_role_policyは上記のようにActionがsts:AssumeRoleWithWebIdentity
、PrincipalのFederatedに先ほど作成したOpenID Connect Providerのarnを指定します。
その次のCondition節がとても重要で、どのリポジトリのどのブランチを契機に実行されたActionかはtoken.actions.githubusercontent.com:sub
に書かれています。
ここをStringLikeによって制限しなければどのようなリポジトリからであってもこのIAMロールのarnを知っているGitHub ActionsであればこのIAMロールを借り受けられるようになってしまいます。
それを防ぐためには上記例の通りtoken.actions.githubusercontent.com:sub
をStringLikeで制限する必要があり、少なくともorg_nameとrepo_nameの指定は必須と言えます。ref_prefixは必要に応じて指定すればよく、たとえばmainブランチだけに制限するならばrefs/heads/main
、特に制限が不要であれば*
でよいでしょう。
GitHub Actionsのトリガーイベントによってはsubにref以外も書かれている場合があり、詳しくはこのGitHubのDocsに書かれています。
後半でIAMロールに付与しているインラインポリシーは次の項で動作を確認するためのIAM権限です。
GitHub Actionsのワークフローを作成する
下記のようにして、GitHub Actionsのワークフローを記述します。
name: "test invoking AWS API through OpenID Connect" on: - push jobs: test-job: runs-on: ubuntu-latest permissions: id-token: write steps: - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v1 with: role-to-assume: arn:aws:iam::123456789012:role/github_actions_open_id_connect_example_role role-session-name: github_actions_open_id_connect_example aws-region: ap-northeast-1 - run: aws sts get-caller-identity
aws-actions/configure-aws-credentialsアクションのrole-to-assume引数に、先ほど作成したIAMロールのarnを指定します。 role-session-name引数はどのリポジトリのGitHub Actionsからの呼び出しかわかるような名前に設定しておくとよいでしょう。
また、AWSから借り受けたアクセスキーをジョブの実行中に格納しておくため、id-token: write
をpermissionsに指定しておく必要があります。
ワークフローの最上位にpermissionsを書いてもよいのですが、その場合ワークフロー全体、すなわちジョブ単位のpermissionsが書かれていない全てのジョブに対してデフォルトでは存在しないid-token: write
の権限が与えられてしまいます。
必要のない権限は与えないに越したことはないので、ジョブ単位に限定したpermissionsへ書いたほうが無難と言えます。
ここではAWSアクセスキーの発行と使用しかしていないため必要なpermissionはid-token: write
のみですが、リポジトリの内容を操作するような別の処理を行う場合にはそれに対応する権限も記述が必要です。
例えばコードをcheckoutする場合はcontents: read
が追加で必要になります。
これらpermissionsについて何が設定できるかは、このGitHubのDocsに書いてあります。
さて、前述のTerraformをapplyしたうえでこのワークフローをGitHubにpushすると、GitHub Actionsは指定したIAMロールの権限を持った有効期限付きのアクセスキーを借り受け、aws cliを通じてstsのget-caller-identityアクションを実行します。その模様が下記のスクリーンショットです。
aws sts get-caller-identity
コマンドの出力のArnのassumed-roleの次に指定したIAMロールのロール名である github_actions_open_id_connect_example_role
が、その次にrole-session-nameで指定したgithub_actions_open_id_connect_example
が入っていることから、OpenID Connectを通じて借り受けたアクセスキーを使っていることが確認できます。
このようにすれば、OpenID Connectを通じて借り受けたアクセスキーを使ってAWSのAPIを操作できます。
あとは必要な権限とStringLikeによる適切なリポジトリの制限を持ったIAMロールを作成すれば、TerraformのplanでもecspressoのデプロイでもServerless Frameworkのデプロイでも、GitHub Actionsで自動的に実行できるようになります。
このOpenID Connectによって新しいリポジトリを作成したときのCDの設定がIAMロールを作成するだけでよくなったため、随分楽になりました。
まとめ
本記事では、GitHub ActionsとOpenID Connectを使ってAWSのAPIをセキュアに操作する利点と、その仕組みをTerraformで構築する手順についてお話しました。
去年秋ごろの正式リリース前の時点で既に一部ではかなりホットな話題だったため、様々なブログやQiita, Zennなどでも結構な数の言及が既になされています。
しかし、最初に出た例がCloudFormationだったりしたことでTerraformである程度まとまった、かつそれぞれのリソースが何をしているのか、どこに注意すべきかまで書いた記事はなかなか見つからなかったので、改めて書いてみました。
これからCI/CDを整備しようというかたの参考になれば幸いです。
ブックウォーカーでは物理・電子・Web連載問わず漫画や本が好き、あるいはテストやデプロイなどの定型業務は自動化に任せ、よりサービスに注力できる状態を作り上げることに興味があるWebアプリケーションエンジニアを募集しています。
興味がありましたらぜひ、採用情報ページからご応募ください。
参考文献
- Tori Hara on Twitter: "何がどのくらい最高かと言いますと! GitHub Actions に AWS クレデンシャルを直接渡さずに IAM ロールが使えるようになることがまず最高で! クレデンシャル直渡しを回避するためだけの Self-hosted runner が必要なくなるところも最高です!!✨✨✨… https://t.co/k7hyjNQEF3"
- GitHub Actions OIDCでconfigure-aws-credentialsでAssumeRoleする | DevelopersIO
- GitHub Actions の OpenID Connect サポートについて
- aws_iam_openid_connect_providerのthumbprint_listの計算方法 - Qiita
- Terraform だけを使って GitHub Actions OIDC ID プロパイダの thumbprint を計算する方法
その他多数