GitHub Actions + AWSでCI-CDを構築する——OIDCでアクセスキーなしの安全なデプロイ

GitHub ActionsからAWSへのデプロイをOIDC認証で実現し、アクセスキーを一切使わない安全なCI-CDパイプラインの構築方法を解説する。

このブログのデプロイはGitHub Actionsで自動化している。mainブランチにpushすると自動でAstroをビルドし、S3にアップロードして、CloudFrontのキャッシュを無効化する。一連の流れを約2分で完了させている。

以前は「GitHub SecretsにAWSのアクセスキーを登録する」方法を使っていたが、今はOIDC(OpenID Connect)認証に切り替えた。この記事では、アクセスキーを一切使わない安全なCI-CDパイプラインの構築方法を解説する。

アクセスキーをGitHub Secretsに置くリスク

AWSのアクセスキーをGitHub Secretsに登録する方法は手軽だが、リスクがある。

  • 鍵の有効期限がない: IAMアクセスキーはデフォルトで有効期限なしに発行される。漏洩しても気づきにくい
  • ローテーションが面倒: 鍵を更新するたびにGitHub Secretsも書き換える必要がある
  • 権限の最小化が難しい: 「とりあえず広めの権限のキー」を使いまわしてしまいがち
  • セキュリティスキャンに引っかかる: コードに誤ってコミットするリスクはゼロではない

OIDCを使えばこれらの問題をすべて解決できる。GitHub ActionsがAWSに対して一時的な認証情報を要求し、その都度短命なトークンで操作する。長期的なアクセスキーを発行しなくて済む。

GitHub OIDC + IAMロールの設定方法

1. IAM OIDCプロバイダーを作成する

AWSコンソールまたはCDKでOIDCプロバイダーを登録する。

// AWS CDKでOIDCプロバイダーを作成する例
import * as iam from 'aws-cdk-lib/aws-iam';

const githubProvider = new iam.OpenIdConnectProvider(this, 'GitHubOidcProvider', {
  url: 'https://token.actions.githubusercontent.com',
  clientIds: ['sts.amazonaws.com'],
});

2. IAMロールを作成する

GitHub Actionsがアサインするロールを作成する。信頼ポリシーで「特定のリポジトリのmainブランチからのみ」に絞ることが重要だ。

const deployRole = new iam.Role(this, 'GitHubActionsDeployRole', {
  assumedBy: new iam.WebIdentityPrincipal(
    githubProvider.openIdConnectProviderArn,
    {
      StringEquals: {
        'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
      },
      StringLike: {
        'token.actions.githubusercontent.com:sub':
          'repo:takashi-endo/taka-techblog:ref:refs/heads/main',
      },
    }
  ),
  roleName: 'GitHubActionsDeployRole',
});

// S3への書き込みとCloudFront Invalidationのみ許可
deployRole.addToPolicy(new iam.PolicyStatement({
  actions: ['s3:PutObject', 's3:DeleteObject', 's3:ListBucket'],
  resources: [
    bucket.bucketArn,
    `${bucket.bucketArn}/*`,
  ],
}));

deployRole.addToPolicy(new iam.PolicyStatement({
  actions: ['cloudfront:CreateInvalidation'],
  resources: [`arn:aws:cloudfront::${this.account}:distribution/${distributionId}`],
}));

StringLikeref:refs/heads/mainを指定することで、mainブランチからのActionsのみがロールを引き受けられる。他のブランチやフォークからの不正な実行を防げる。

GitHub Actionsワークフローの実装

以下が実際に使っているワークフローの全体像だ。

name: Deploy to S3

on:
  push:
    branches:
      - main

permissions:
  id-token: write   # OIDCトークンの発行に必要
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeployRole
          aws-region: ap-northeast-1

      - name: Deploy to S3
        run: |
          aws s3 sync ./dist s3://taka-techblog \
            --delete \
            --cache-control "public, max-age=31536000, immutable" \
            --exclude "*.html" \
            --exclude "*.xml" \
            --exclude "*.json"
          aws s3 sync ./dist s3://taka-techblog \
            --delete \
            --cache-control "public, max-age=0, must-revalidate" \
            --include "*.html" \
            --include "*.xml" \
            --include "*.json"

      - name: Invalidate CloudFront cache
        run: |
          aws cloudfront create-invalidation \
            --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
            --paths "/*"

ポイント解説

permissions: id-token: writeが必須

OIDCトークンを発行するためにid-token: writeの権限が必要だ。これを忘れると認証エラーが出る。

キャッシュコントロールを分けてS3にアップロードする

S3のsyncコマンドを2回実行している理由は、ファイル種別ごとにキャッシュ設定を変えるためだ。

  • HTMLやXML・JSONはmax-age=0で常に最新を取得させる
  • 画像・CSS・JSはハッシュ付きファイル名が多いためmax-age=31536000で長期キャッシュ

この設定でCloudFrontのキャッシュを最大限活用しつつ、コンテンツの更新を確実に反映できる。

CloudFront IDはSecretsに入れる

ディストリビューションIDはAWSリソースの識別子で、セキュリティ上の機密ではないが、公開リポジトリの場合はSecretsに入れておくのが無難だ。

ブランチ戦略

このブログでは以下のシンプルな戦略を採用している。

ブランチ役割デプロイ先
main本番S3(本番)
feature/*作業ブランチデプロイなし(ビルド確認のみ)

記事の草稿はfeature/article-nameブランチで作業し、完成したらmainにマージする。マージと同時にGitHub Actionsが起動して本番デプロイが完了する。

複数人開発やステージング環境が必要な場合は、developブランチをステージング環境にデプロイするジョブを追加する構成が一般的だ。

on:
  push:
    branches:
      - main      # 本番デプロイ
      - develop   # ステージングデプロイ

規制産業のCI-CD——「誰がいつデプロイしたか」をトレースできる仕組み

治験管理システムにCI-CDを導入したとき、一般的なWebサービスにはない要件が加わった。「リリース記録」だ。

医療系システムは変更管理のプロセスが厳格で、「いつ、誰の承認のもとで、何をリリースしたか」をトレースできる証跡が必要だ。GitHub Actionsのワークフロー実行履歴は、そのままリリース記録として機能することに気づいた。

ポイントはデプロイを main ブランチへのマージに紐づけることだ。GitHubのPR(プルリクエスト)にはレビュー承認の記録が残り、マージした時刻・マージした人物が履歴として残る。main へのマージをトリガーにデプロイすることで、「このリリースはPR #123のレビューを通過し、承認者Aのレビューを経て、Bがマージした」という証跡が自動で残る。

もうひとつ取り組んだのがデプロイの冪等性だ。CI-CDを導入する前は手動デプロイで、「さっきと同じコマンドを打ったのになぜか動作が変わった」という事象が起きていた。GitHub Actionsを通じたデプロイは、同じコミットからは常に同じ結果が得られる。これは規制要件の「再現性」とも合致している。

アクセスキーの廃止(OIDC導入)も、規制対応として重要だった。アクセスキーはローテーションが必要で、管理が煩雑だ。OIDCで一時トークンを発行する方式にしてからは、「認証情報の管理」という運用コストが完全になくなり、外部監査でも「適切な認証管理がされている」と評価された。

まとめ

GitHub Actions + AWS OIDCによるCI-CDパイプラインのポイントをまとめる。

  • アクセスキーはSecretsに置かない: OIDCで一時認証情報を使う設計にする
  • IAMロールの信頼ポリシーはリポジトリとブランチで絞る: StringLikeでsub条件を設定する
  • permissions: id-token: writeを忘れない: OIDCトークン発行に必須
  • S3のキャッシュ設定はファイル種別で分ける: HTMLと静的アセットで戦略を分ける
  • デプロイとInvalidationはセット: これを忘れると古いファイルが配信され続ける

一度設定してしまえば、あとはgit pushするだけで本番反映が完了する。開発体験が大きく向上するので、まだ手動デプロイをしているなら今すぐCI-CDを導入することを強くすすめる。