😤

「GitHub Actions × AWS」のトレーサビリティ向上委員会

2023/03/09に公開

GitHub ActionsからAWSを操作できると便利です。OIDC(OpenID Connect)を使えばアクセスキーも不要で、セキュアに運用できます。しかし複数システムにまたがる特性上、トラブルシューティングは面倒です。そこで本記事ではトレーサビリティを向上させ、メンテナンスしやすくする手法を紹介します。トレーサビリティが向上すれば、運用はもっと楽になります。

GitHub ActionsとAWSは赤の他人

当たり前ですが、GitHub ActionsとAWSはまったく異なるシステムです。何を言っているんだという感じですが、トレーサビリティの観点では重要です。なぜなら複数のシステムを扱うときは、トレースできるよう設計しないとトレーサビリティは生まれないからです

GitHub ActionsやAWSに限らず、普通は他所様のシステムなど知ったこっちゃありません。このお互いに知らんぷりを決め込んでいるシステムの仲立ちは、我々の仕事になります。まったくもって手のかかるヤツらですね。

Configure AWS Credentialsアクション

GitHub ActionsからAWSへアクセスするには、Configure AWS Credentialsアクションが便利です。認証方式にはもちろんOIDCを使います。OIDCでAWSへアクセスするには、事前にAWS側でOpenID Connect ProviderとIAMロールのセットアップが必要です。本記事では、これらのリソースは準備済みという前提で話を進めます[1]

GitHub Actionsのワークフローでは、次のようにConfigure AWS Credentialsアクションを呼び出します[2]。これだけでAWSのクレデンシャルが払い出されます。

- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@v2
  with:
    role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-actions-role
    aws-region: ap-northeast-1

各入力パラメータには次の値を指定します。

  • role-to-assume:事前に作成しておいたIAMロールのARN
  • aws-region:デフォルトリージョン

ここで使用しているIAMロール名は「github-actions-role」です。AWSアカウントIDはコンフィギュレーション変数(Configuration Variables)へ事前に追加しておいたものを利用します。

単に動かすだけならこれで終わりですが、お世辞にもトレーサビリティが高いとはいえません。そこでこのコードをデフォルト設計として、少しずつ改善していきましょう。

トレーサビリティとはなにか

GitHub ActionsやAWSは、自身のシステムについてはよく知っています。問題は自身のシステムと相手側システムの関連付けです。ある事象が自システムで起きた時、相手側で何が起きていたかどうやって知ればよいでしょうか。ここで登場するのがトレーサビリティです。本記事ではトレーサビリティを次のように定義します。

  • GitHub Actionsの実行ログから、AWS CloudTrail[3]のログをたどれる
  • AWS CloudTrailのログから、GitHub Actionsの実行ログをたどれる

つまりどちらかのログを眺めているときに、関連する相手側のログを探し出せればOKです。このとき重要なのは、どれだけ簡単に相手側のログにたどり着けるかです。実運用を考慮すると、可能な限り詳細を知らなくてもたどれる状態が望ましいでしょう。なぜならログを参照するのは、内部実装に詳しい人とは限らないからです。

くどくど書きましたが、そろそろ実践に移りましょう。設計のポイントは大きく3つです。

  1. セッション名にGitHub Actionsの情報を付与する
  2. AWSアカウントIDをGitHub Actionsのログに書き出す
  3. AWS CloudTrailのURLをGitHub Actionsのステップサマリーへ書き出す

セッション名の設定

Configure AWS Credentialsアクションでは入力パラメータに、セッション名を指定できます。ここで設定したセッション名は、AWS CloudTrailが自動的にログへ含めてくれます。

そこでオススメなのは「セッション名にGitHub Actionsの情報を含めてしまう」ことです。そうすればAWS CloudTrailから、一意にGitHub Actionsの実行ログへ到達できます。セッション名には任意の値を組み込めますが、筆者は次のような実装にしています。

- name: Generate session name
  id: session
  run: |
    repo="${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}"
    echo "name=${repo}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_OUTPUT}"

- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@v2
  with:
    role-session-name: ${{ steps.session.outputs.name }}
    ......

Generate session nameステップでセッション名を生成し、Configure AWS Credentialsアクションのrole-session-nameキーへ渡します。セッション名は次のような構造で組み立て、GitHub Actionsとの関連付けに必要な情報を詰め込みます[4]

これは相関IDの役割を担います。各要素の意味は次のとおりです。

  • リポジトリ名:<org-name>/<repo-name>の「<repo-name>」部分[5]
  • ワークフロー実行ID:GitHub Actionsのワークフロー実行時に生成される識別子
  • ワークフロー試行回数:何度目のワークフロー実行か[6]

なおセッション名には任意の値を組み込めると述べましたが、使える文字種や長さには制限があります。使用可能文字は半角英数と特殊文字「=,.@-_」で、最長64文字です[7]

これをexampleというリポジトリで実装すると、たとえばexample-4181401234-1のようなセッション名が生成されます。またAWS CloudTrailには、次のようなログが記録されます。

{
  "eventVersion": "1.08",
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "XXXXXXXXXXXX:example-4181401234-1",
    "arn": "arn:aws:sts::123456789012:assumed-role/github-actions-role/example-4181401234-1",
    "accountId": "123456789012",
    "accessKeyId": "XXXXXXXXXX",
    "sessionContext": {
      "sessionIssuer": {
        "type": "Role",
        "principalId": "XXXXXXXXXXXX",
        "arn": "arn:aws:iam::123456789012:role/github-actions-role",
        "accountId": "123456789012",
        "userName": "github-actions-role"
      },
      ......
    }
  },
  "eventTime": "2023-02-15T07:02:13Z",
  "eventSource": "sts.amazonaws.com",
  "eventName": "GetCallerIdentity",
  "awsRegion": "ap-northeast-1",
  ......
}

このようにAWS CloudTrailのログを見れば、GitHub Actionsの実行ログと関連付けが可能です。ただし可能なだけで、たどりやすいわけではありません。GitHub Actionsの実行ログへアクセスするには、次のような規則でURLを自力で組み立てます。

とはいえGitHub Actions側のログを一意に識別できるだけでも、何もやらないよりマシです。普段はAWSからGitHub Actionsへたどる頻度も多くないため、出発点としては悪くありません。

ちなみにConfigure AWS Credentialsアクションでは、セッション名を省略するとデフォルト値「GitHubActions」が使われます。AWS側からGitHub Actionsの実行ログを一意に識別する情報は、何も記録されません。時刻で何となく当たりをつける運用になります。イマイチです。

AWSアカウントIDの出力

次にGitHub ActionsからAWSへたどりやすくします。あまり知られていませんが、Configure AWS Credentialsアクションを使うと、ログ出力時にAWSアカウントIDをマスクしてしまいます

masked-log

おそらくセキュリティ向上のため、このような設計になっていると思われます[8]。しかしトレーサビリティの観点ではイマイチです。複数AWSアカウントの運用は珍しくないためです。AWSアカウントIDがマスクされると、どのAWSアカウントを操作したか不明瞭になります。

そこでマスク処理を無効化しましょう。次のようにConfigure AWS Credentialsアクションのmask-aws-account-idキーで設定します。

uses: aws-actions/configure-aws-credentials@v2
with:
  mask-aws-account-id: false
  ......

ただしお気づきのとおり、マスク処理の有無にはトレードオフがあります。

  • マスク処理の有効化
    • メリット:万が一ログが漏れても、第三者にAWSアカウントIDが渡らない
    • デメリット:どのAWSアカウントを操作したか分からなくなる
  • マスク処理の無効化
    • メリット:どのAWSアカウントを操作したか明確に分かる
    • デメリット:万が一ログが漏れると、第三者にAWSアカウントIDが渡る

AWSアカウントIDをどれだけセンシティブに扱うかで、評価は変わります。個人的にはプライベートリポジトリであれば、AWSアカウントIDはログ出力したほうがよいと考えています。ログが漏れてる時点で、AWSアカウントIDだけ秘密にできても効果に疑問符がつくからです。どうせセキュリティ的にたいした効果がないなら、トレーサビリティを向上させるのは悪くない判断です[9]

AWS CloudTrailのURL出力

先ほど紹介したセッション名を使うと実は、AWS CloudTrailのログがフィルタリングできます。AWSマネジメントコンソールにログインした状態で、次のURLをブラウザで開きましょう。

するとGitHub Actions経由のログだけにフィルタリングされます。かなり便利です。

cloudtrail

URLは手動で組み立ててもよいですが、ステップサマリーに出力するのもオススメです。AWSアカウントIDと一緒に出力すれば、ログの場所が一目瞭然です。たとえば次のコードを追加します。

- name: Output step summary
  env:
    AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
    SESSION_NAME: ${{ steps.session.outputs.name }}
    CLOUDTRAIL_URL: https://console.aws.amazon.com/cloudtrail/home#/events
  run: |
    echo "## Authorized on AWS (${AWS_ACCOUNT_ID})" >> "${GITHUB_STEP_SUMMARY}"
    echo "- ${CLOUDTRAIL_URL}?Username=${SESSION_NAME}" >> "${GITHUB_STEP_SUMMARY}"

ワークフローを実行するとGitHub Actionsのログページに、次のようなリンクが出てきます。

step-summary

このリンクをクリックすれば、GitHub Actions経由の操作ログが簡単に確認できます。AWSアカウントへの事前ログインは必要ですが、かなりトラブルシューティングがしやすくなります。なおステップサマリーへの出力ではなく、プルリクエストのコメントに貼り付けても便利です。

ログ保存期間

少し脇道にそれますが、ログ保存期間に触れておきます。たとえば社内に「本番環境の操作ログは半年間保持する」などの規定がある場合は要注意です。AWS CloudTrailのログ保存期間は正しく設定しているのに、GitHub Actions側の設定を忘れるケースがあります。

GitHub Actionsのログ保存期間はデフォルトで90日です。これで足りない場合は、明示的な設定が必要です。ログ保存期間はリポジトリ単位で設定できます。Setting the retention period for a repositoryを参考に、適切なログ保存期間を設定しておきましょう[10]

log-settings

すべてを統合する

それではここまでの内容を統合します。まずは次の事前準備を行いましょう。

  • OpenID Connect ProviderとIAMロールをAWSに作成する
  • AWS_ACCOUNT_IDという名前でコンフィギュレーション変数を設定する

そのうえで次のようなコードを実装します。

name: AWS
on: push
jobs:
  aws:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      contents: read
      id-token: write
    steps:
      - name: Generate session name
        id: session
        run: |
          repo="${GITHUB_REPOSITORY#${GITHUB_REPOSITORY_OWNER}/}"
          echo "name=${repo}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "${GITHUB_OUTPUT}"

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          mask-aws-account-id: false
          role-session-name: ${{ steps.session.outputs.name }}
          role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-actions-role
          aws-region: ap-northeast-1

      - name: Output step summary
        env:
          AWS_ACCOUNT_ID: ${{ vars.AWS_ACCOUNT_ID }}
          SESSION_NAME: ${{ steps.session.outputs.name }}
          CLOUDTRAIL_URL: https://console.aws.amazon.com/cloudtrail/home#/events
        run: |
          echo "## Authorized on AWS (${AWS_ACCOUNT_ID})" >> "${GITHUB_STEP_SUMMARY}"
          echo "- ${CLOUDTRAIL_URL}?Username=${SESSION_NAME}" >> "${GITHUB_STEP_SUMMARY}"

      - name: Run AWS CLI
        run: |
          aws sts get-caller-identity

たいしたコード量ではありませんが、細かい配慮が随所に散りばめられています。ひとつひとつは小さな工夫ですが、デフォルト設計と比べれば大幅にトレーサビリティが向上しています。

まとめ

本記事ではGitHub ActionsとAWS連携時の、トレーサビリティ向上手法を3つ紹介しました。

  1. セッション名にGitHub Actionsの情報を付与する
  2. AWSアカウントIDをGitHub Actionsのログに書き出す
  3. AWS CloudTrailのURLをGitHub Actionsのステップサマリーへ書き出す

これにより次のことが実現できました。

  • GitHub Actionsの実行ログから、AWS CloudTrailのログをたどれる
  • AWS CloudTrailのログから、GitHub Actionsの実行ログをたどれる

特にGitHub ActionsからAWS CloudTrailのログへたどるのは簡単で、トラブルシューティングも楽になります。たいして手間がかからないわりに、効果はバツグンです。ぜひお試しください。

脚注
  1. 詳細はConfiguring OpenID Connect in Amazon Web Servicesなどを参照してください。 ↩︎

  2. 本記事では読みやすさを優先して、サードパーティアクションの呼び出しはタグで行っています。しかし本番運用ではcommit SHAで固定すべきです。詳細はSecurity hardening for GitHub ActionsのUsing third-party actionsを参照しましょう。 ↩︎

  3. AWS CloudTrailはAWSの監査ログサービスです。AWS APIの実行を記録するサービスです。もちろんGitHub ActionsからAWS APIを実行した場合のログも残ります。詳細は公式ドキュメントを参照してください。 ↩︎

  4. コード内のGITHUB_で始まる環境変数は、デフォルト環境変数です。GitHub Actions実行時に勝手に値がセットされます。 ↩︎

  5. GitHub Actionsではなぜか、org名抜きのリポジトリ名がデフォルト環境変数で取得できません。そのためシェル芸を駆使して、無理やりリポジトリ名を取得しています。分かりづらいのは重々承知してるんですが、他に方法が…。 ↩︎

  6. ワークフローは再実行できます。そのためGitHub Actionsの実行ログを一意に識別する際、必要な情報になります。 ↩︎

  7. セッション名の仕様はAssumeRoleWithWebIdentity APIのRoleSessionNameに記載されています。 ↩︎

  8. 歯切れが悪くて恐縮ですが、コミットログを追いかけてもなぜマスクするようにしたのか記録が残っていませんでした。 ↩︎

  9. もちろん結論は取り巻く状況によって変わるでしょうが、少なくとも検討してみる価値はあります。 ↩︎

  10. ログ保存期間はOrganization単位でも設定は可能です。手順はConfiguring the retention period for GitHub Actions artifacts and logs in your organizationにあります。あなたがOrganization管理者なら、まとめて設定するとより確実です。 ↩︎

Discussion