GitHub Actions で Docker ビルド + ECR push を自動化する

概要

アプリケーションを ECS や Lambda(コンテナイメージ)にデプロイする際、コンテナイメージのビルドと Amazon ECR への push を手作業でやっていると、ローカル環境差によるビルドのぶれ、push し忘れ、タグの取り違え、そして「今動いているのはどのコミット由来か分からない」という追跡性の問題が起きがちです。

この記事では、GitHub Actions で「main ブランチへの push をトリガーに Docker イメージをビルドし、Amazon ECR へ push する」までを、OIDC による短命クレデンシャル方式で構築します。長期の IAM アクセスキーを GitHub Secrets に置かない、現在の推奨構成です。

環境

  • GitHub Actions(ubuntu-latest ランナー)
  • Amazon ECR(プライベートリポジトリ)
  • AWS 認証は OIDC(IAM ロールを AssumeRole)
  • イメージビルドは docker/build-push-action(Buildx)
  • リージョンは例として ap-northeast-1

アカウントID・リポジトリ名は例示値です。実運用では自環境の値に読み替えてください。

なぜ OIDC 方式なのか

従来の「IAM アクセスキーを Secrets に登録する」方式は動作しますが、長期の秘密を外部サービスに預け続けることになり、漏洩時のダメージとローテーション負荷が大きいという弱点があります。

OIDC 方式では、ジョブ実行のたびに GitHub が発行する OIDC トークンと引き換えに、AWS STS が短命の一時クレデンシャルを払い出します。GitHub 側に長期の秘密を持たなくて済むのが最大の利点です。

ステップ1: OIDC プロバイダを登録する

アカウントに未登録の場合のみ実行します(1アカウントに1つで共用可能)。

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com \
  --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1

thumbprint は変わることがあります。Terraform 等の IaC では thumbprint を省略できるケースもあるため、最新のドキュメントを確認してください。

ステップ2: AssumeRole 用の IAM ロールを作る

信頼ポリシーで「どのリポジトリの、どの ref からのトークンを許可するか」を絞り込むのが肝心です。

trust-policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:my-org/my-app:ref:refs/heads/main"
        }
      }
    }
  ]
}

注意: sub の条件を repo:my-org/my-app:* のようにワイルドカードで広げると、そのリポジトリのどのブランチ・PR・タグからでも AssumeRole できてしまいます。push トリガーで main だけを許可したいなら ref:refs/heads/main まで絞り込みましょう。

ロール作成とポリシーのアタッチ:

aws iam create-role \
  --role-name github-actions-ecr-push \
  --assume-role-policy-document file://trust-policy.json

aws iam put-role-policy \
  --role-name github-actions-ecr-push \
  --policy-name ecr-push \
  --policy-document file://ecr-policy.json

ecr-policy.json(最小権限):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage"
      ],
      "Resource": "arn:aws:ecr:ap-northeast-1:123456789012:repository/my-app"
    }
  ]
}

ecr:GetAuthorizationToken はリソース単位で絞れないため Resource: "*" になりますが、push 系アクションはリポジトリ ARN で絞れます。

ステップ3: ECR リポジトリを用意する

aws ecr create-repository \
  --repository-name my-app \
  --region ap-northeast-1 \
  --image-scanning-configuration scanOnPush=true

scanOnPush=true にすると push のたびに脆弱性スキャンが走ります。

ステップ4: ワークフローを書く

.github/workflows/build-and-push.yml:

name: Build and Push to ECR

on:
  push:
    branches: [main]

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

env:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: my-app

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

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-ecr-push
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
            ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

ポイント

permissions: id-token: write は必須 これが無いと OIDC トークンが発行されず、AssumeRole が失敗します。ハマりどころの筆頭です。ジョブレベルまたはワークフローレベルで明示してください。

タグにコミットSHAを付ける :latest だけだと稼働イメージがどのコミット由来か追えません。${{ github.sha }} を主タグにし、:latest を補助にすると、ロールバックもタグ指定で確実に行えます。

Buildx の GitHub Actions キャッシュ cache-from: type=gha / cache-to: type=gha,mode=max でレイヤーキャッシュを保存し、変更のないレイヤーの再ビルドをスキップできます。

確認方法

  1. main に push し、Actions タブでワークフローが緑になることを確認
  2. ECR にイメージが増えているか確認
aws ecr list-images \
  --repository-name my-app \
  --region ap-northeast-1
  1. configure-aws-credentials のログで、意図した ARN の一時クレデンシャルを取得できているか確認(キー自体はマスクされます)

注意点

  • 信頼ポリシーの sub を広げすぎない。ブランチ・タグ単位で絞る。
  • GetAuthorizationToken の Resource: "*" は仕様上避けられない。push 系だけ ARN で絞る。
  • キャッシュを効かせても Dockerfile のレイヤー順が悪いと効かない。変わりにくいレイヤー(依存インストール)を上、変わりやすいレイヤー(アプリコード COPY)を下に置く。
  • マルチアーキ(arm64 等)が必要なら platforms: を指定するが、QEMU エミュレーションで遅くなる。ネイティブが必要ならセルフホストランナーやマトリクスを検討する。
  • latest 固定運用は上書きで前イメージを辿れなくなる。SHA タグ併用を推奨。

まとめ

  • OIDC + AssumeRole で長期キーを GitHub に置かずに ECR へ push できる
  • 信頼ポリシーの sub 条件の絞り込みがセキュリティの要
  • docker/build-push-action + GitHub Actions キャッシュでビルドを短縮できる
  • SHA タグ運用でデプロイとロールバックの追跡性が上がる

この土台があれば、後段に ECS の update-service や Lambda の update-function-code を足すだけで、push からデプロイまで一気通貫にできます。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。