GitHub ActionsからAWS APIへアクセスキーなしでリクエストする仕組みをTerraformで構築する

こんにちは。

メディアサービス開発部バックエンド開発グループのフサギコ(髙﨑)です。

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を操作しようとすると、

  1. 必要な権限を持ったCI/CD用のIAMユーザを作成
  2. IAMユーザのアクセスキーを生成しダウンロード
  3. リポジトリの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アプリケーションエンジニアを募集しています。

興味がありましたらぜひ、採用情報ページからご応募ください。

www.bookwalker.co.jp

参考文献

その他多数