Contents
GitHub Actionsでのワークフロー設計と責務分離
CI はビルド・テスト・静的解析・SBOM・脆弱性スキャン・署名までを担い、CD は署名済みの不変イメージを参照してデプロイします。こうすることで再現性と追跡性が確保できます。
イベントとブランチ戦略
トリガーはフィードバック速度とコストのバランスで決めます。PR では軽量な検査、main/tags ではフルパイプラインを回すのが実務的です。
- pull_request: PR プレビュー、ユニットテスト、軽量ビルド、SBOM 生成。PR クローズでプレビュー環境破棄。
- push (feature/develop): 開発用タグをレジストリへ push。CI を回して早期検出。
- push (main) / tags (v..*) / release: 本番向けのフルビルド(マルチアーキ、深いスキャン、署名)、承認フローでデプロイ。
- workflow_dispatch / schedule: 手動実行や定期スキャン用。
運用上のポイント
運用面は承認・並列制御・タイムアウト設計が重要です。環境ごとの保護設定や concurrency、timeout-minutes を適切に設定してください。
- Environments を使って承認フローを設定する。
- concurrency を使って同一ブランチの重複実行を防ぐ。
- job ごとに timeout-minutes と retry パターンを定義する。
実務的なDockerfileの書き方とテスト実行
ランタイムとビルド依存を分離するマルチステージビルドが基本です。HEALTHCHECK 実装やビルド中のテスト実行、最小限のランタイムイメージ設計を優先してください。
マルチステージとレイヤー設計
変化しにくいレイヤーを上に、変化しやすいソースを下に置きます。テストはビルドステージ内で実行することで最終イメージに devDependencies を残さずにテスト可能です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# build stage FROM node:18-bullseye-slim AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --no-audit --no-fund COPY . . RUN npm run build # test stage: devDependencies を使ってテストを実行する FROM node:18-bullseye-slim AS test WORKDIR /app COPY package*.json ./ RUN npm ci --no-audit --no-fund COPY . . # テストで失敗するとビルドが失敗するため CI が止まる RUN npm test # runtime stage: minimal FROM node:18-slim AS runtime ENV NODE_ENV=production WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/healthcheck.js /usr/local/bin/healthcheck.js RUN chmod +x /usr/local/bin/healthcheck.js USER node HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD node /usr/local/bin/healthcheck.js CMD ["node", "dist/index.js"] |
上の構成では、docker build --target test でテストステージを実行し、成功すれば docker build --target runtime で最終イメージを作成できます。
HEALTHCHECK:curl をインストールする例
ランタイムに curl を入れる場合は最小限に留め、キャッシュと不要なパッケージを残さないようにします。curl が無いイメージもあるため注意が必要です。
|
1 2 3 4 5 6 7 8 |
FROM node:18-slim RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/* # ヘルスチェックは curl の終了コードで判定する。冗長な "|| exit 1" は不要。 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD curl -fsS http://127.0.0.1:3000/health >/dev/null |
インストールはイメージサイズを増やすので、必要性を評価してください。
HEALTHCHECK:Node スクリプトで実装する例
Node アプリなら小さなスクリプトでヘルスチェックを書く方が軽量です。ランタイムに追加のパッケージを入れずに済みます。
|
1 2 3 4 5 6 7 8 9 10 |
// healthcheck.js #!/usr/bin/env node const http = require('http'); const req = http.get('http://127.0.0.1:3000/health', { timeout: 3000 }, res => { if (res.statusCode === 200) process.exit(0); else process.exit(1); }); req.on('error', () => process.exit(1)); |
このスクリプトを最終イメージへコピーして HEALTHCHECK で実行します。HEALTHCHECK のコマンドは 0/非0 の返り値で判定されるため、|| exit 1 のような冗長表現は不要です。
テストをビルドステージで実行する方法
テストは最終イメージに devDependencies を残すのではなく、ビルド工程の test ステージで実行してください。GitHub Actions では buildx を使って target: test をビルドさせるとよいです。
例(GitHub Actions のステップ):
|
1 2 3 4 5 6 7 8 9 |
- name: Run tests in build stage uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile target: test push: false platforms: linux/amd64 |
このステップで npm test が失敗すればパイプラインが停止します。最終イメージにはテストツールが残りません。
GitHub ActionsでのDockerビルドとキャッシュ設計
buildx と registry/cache、actions/cache を組み合わせると高速かつ安定したビルドが可能です。キャッシュキーの設計と無効化方法をあらかじめ決めて運用できます。
buildx と docker/build-push-action の使い方
buildx はマルチアーキとレイヤーキャッシュを有効にします。Action は将来的な破壊的変更を回避するため、必要に応じてコミット SHA やリリース版(パッチ指定)で固定してください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- uses: docker/setup-buildx-action@v2 # 例。運用では commit SHA 固定を検討 - uses: docker/setup-qemu-action@v2 - uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile push: true platforms: linux/amd64,linux/arm64 tags: | ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} cache-from: type=registry,ref=${{ env.BUILD_CACHE }} cache-to: type=registry,ref=${{ env.BUILD_CACHE }},mode=max |
注記: @v2 / @v4 などのメジャーバージョンのみ指定すると将来の破壊的変更を取り込む可能性があります。重要な CI に組み込む場合は、定期的にリリースノートを確認し、安定したコミット SHA やリリースタグで固定する運用を検討してください。
actions/cache のキー設計と無効化方法
依存キャッシュにはハッシュを使い、復元キーも用意します。キャッシュ無効化は「バージョン文字列を増やす」か、ファイルハッシュを変えることで行います。
例(node_modules):
|
1 2 3 4 5 6 7 8 |
- name: Cache node modules uses: actions/cache@v3 with: path: ~/.npm key: node-modules-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}-v1 restore-keys: | node-modules-${{ runner.os }}- |
設計例:
- 依存キャッシュキー = OS + lockfile ハッシュ + キャッシュバージョン
- レイヤーキャッシュ(buildx) = registry の ref を使い、
BUILD_CACHEにrepo:buildcache-v1のようなバージョンを付与して必要時にv2へ切り替える
キャッシュの破損時にはキーを更新して強制的に再構築させるフローを用意してください。
レジストリ連携・認証・署名の運用
レジストリへの push/プルはパーミッションと組織ポリシーに影響されます。OIDC を使った短期認証を第一選択とし、PAT はポリシーが許さない場合の代替としてください。
GHCR の注意と権限
ワークフローで GHCR に push する場合、ワークフロー権限と組織ポリシーに注意が必要です。最低限 workflow の permissions に packages: write を設定してください。また、組織が GITHUB_TOKEN によるパッケージ公開を制限している場合は PAT を用意する必要があります。
例(workflow permissions):
|
1 2 3 4 5 |
permissions: contents: read packages: write id-token: write |
組織や Enterprise のポリシーで GITHUB_TOKEN によるパッケージ作成が無効化されている場合は、リポジトリシークレットに PAT(最小スコープで packages:write 等)を用意して docker/login-action に渡す運用が必要です。PAT のスコープは GitHub の公式ドキュメントで最新仕様を確認してください。
AWS ECR OIDC と KMS の前提と設定
AWS に対して OIDC で短期クレデンシャルを使い、KMS を署名キーに使う場合は IAM ロールの trust ポリシーと KMS キーポリシー両方が正しく設定されている必要があります。主な前提と検証手順を示します。
- OIDC プロバイダー(token.actions.githubusercontent.com)を AWS アカウントに登録する。
- 最小権限の IAM ロールを作成し、trust ポリシーで GitHub Actions のサブジェクト(repo:OWNER/REPO:ref:refs/heads/main など)を限定する。
- KMS キーのキーポリシーに IAM ロールを明示的に許可(kms:Sign, kms:GetPublicKey, kms:Verify 等)する。
例(IAM ロールの信頼ポリシー、プレースホルダは置換してください):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringLike": { "token.actions.githubusercontent.com:sub": "repo:OWNER/REPO:*" }, "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com" } } } ] } |
例(KMS キーポリシーの該当ステートメント):
|
1 2 3 4 5 6 7 8 9 10 |
{ "Sid": "AllowGitHubActionsRoleToSign", "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::ACCOUNT_ID:role/GitHubActionsRole" }, "Action": ["kms:Sign", "kms:GetPublicKey", "kms:Verify"], "Resource": "*" } |
検証方法の例:
- GitHub Actions 上で
aws sts get-caller-identityを実行してロールの取得を確認する。 aws kms get-public-key --key-id arn:aws:kms:REGION:ACCOUNT_ID:key/KEY_IDが成功するかを確認する。- cosign の署名試行(検証用)を実行して KMS にアクセスできるか確認する。
KMS のキーポリシーと IAM ロールの不一致が最も多いトラブル原因なので、テストを用意しておくことを推奨します。
セキュリティ運用と脆弱性対応ワークフロー
SBOM、脆弱性スキャン、署名は検出だけで終わらせず、発見時のエスカレーションやチケット連携までを含めた運用設計が必要です。
SBOM と脆弱性スキャンの自動化
SBOM は syft 等で生成し、Trivy 等でスキャンします。Trivy を使う場合は DB を事前に更新してからスキャンすることで検出精度が上がります。
例(Trivy の実行例):
|
1 2 3 4 5 6 |
# DB 更新(CI 内で一度だけ) docker run --rm aquasec/trivy:v0.41.1 --download-db-only # イメージスキャン docker run --rm aquasec/trivy:v0.41.1 image --format json --output trivy.json --severity HIGH,CRITICAL ${IMAGE} |
スキャン結果はアーティファクトとして保存し、担当者へ通知または Issue 作成のトリガーとします。
脆弱性発見時の運用フローとチケット連携
発見時フローの例を決めておきます。自動化できる部分は自動化し、判定が必要な部分は人間のワークフローへつなぎます。
- 重大(HIGH/CRITICAL): パイプラインを失敗させ、Issue を自動作成してアサインする。
- 中程度(MEDIUM): 自動で警告、定期レビューで対応。
- False positive: .trivyignore や例外管理でトラッキングし、False Positive の根拠を Issue に紐づける。
自動 Issue 作成は peter-evans/create-issue-from-file 等のアクションで実現できます。スキャン DB(例: Trivy DB)は定期的に更新し、更新済みデータに基づく再スキャンをスケジュールしてください。
署名の導入と cosign の安全な導入
cosign を導入する場合、バイナリを curl で直接落として /usr/local/bin に置く方法は検証が不足しがちです。以下のいずれかの方法で検証を行ってください。
- 公式リリースのチェックサム(SHA256)と署名(.sig/GPG)を取得して検証する。署名の公開鍵指紋は公式リポジトリのドキュメントで確認し、鍵のフィンガープリントを事前に照合してください。
例(概念的な手順):
|
1 2 3 4 5 6 7 8 9 |
# バイナリとチェックサム取得 curl -sSL -o cosign https://github.com/sigstore/cosign/releases/download/vX.Y.Z/cosign-linux-amd64 curl -sSL -o cosign.sha256 https://github.com/sigstore/cosign/releases/download/vX.Y.Z/cosign-linux-amd64.sha256 sha256sum --check cosign.sha256 # 署名検証(公開鍵は公式情報で確認して取得) gpg --keyserver hkps://keys.openpgp.org --recv-keys <MAINTAINER_KEYID> gpg --verify cosign.sha256.sig cosign.sha256 |
- 公式のコンテナイメージ(または GitHub Action)を利用する。イメージは digest 固定(@sha256:...)で利用するのが安全です。コンテナ実行時は必要な一時クレデンシャルのみを渡します。
どちらの方法でも「署名/チェックサムを検証する」か「信頼できる配布チャネル(公式 Action / 公式コンテナ)をダイジェスト固定で使う」ことを必須にしてください。
ワークフロー最小テンプレート(動くコピー&ペースト)
ここは動作に必要な最低限のワークフロー例です。実運用ではシークレット名やバージョン固定、組織ポリシーに合わせて調整してください。Action のバージョンは将来の破壊的変更を避けるため、可能な範囲でコミット SHA またはパッチ指定で固定し、定期的にレビューしてください。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
name: CI: build/test/scan/sign on: pull_request: types: [opened, synchronize, reopened] push: branches: [main, develop] workflow_dispatch: {} permissions: contents: read packages: write id-token: write env: IMAGE_REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository_owner }}/myapp BUILD_CACHE: ghcr.io/${{ github.repository_owner }}/myapp:buildcache-v1 jobs: build: runs-on: ubuntu-latest outputs: image: ${{ steps.set-image.outputs.image }} steps: - uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v2 - name: Set up buildx uses: docker/setup-buildx-action@v2 - name: Login to registry uses: docker/login-action@v2 with: registry: ${{ env.IMAGE_REGISTRY }} username: ${{ github.actor }} # 組織ポリシーによっては GITHUB_TOKEN で失敗する場合があるため、PAT を secrets に入れて使う運用も検討する password: ${{ secrets.GITHUB_TOKEN }} - name: Run tests in build stage uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile target: test push: false platforms: linux/amd64 - name: Build and push (multi-arch, cache) id: build uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile platforms: linux/amd64,linux/arm64 push: true tags: | ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ github.run_id }} ${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} cache-from: type=registry,ref=${{ env.BUILD_CACHE }} cache-to: type=registry,ref=${{ env.BUILD_CACHE }},mode=max - name: Set image output id: set-image run: | echo "image=${{ env.IMAGE_REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ github.run_id }}" >> $GITHUB_OUTPUT scan: needs: build runs-on: ubuntu-latest steps: - name: Download Trivy DB run: | docker pull aquasec/trivy:v0.41.1 docker run --rm aquasec/trivy:v0.41.1 --download-db-only - name: Trivy scan (container) env: IMAGE: ${{ needs.build.outputs.image }} run: | docker run --rm aquasec/trivy:v0.41.1 image --format json --output trivy.json --severity HIGH,CRITICAL $IMAGE || true # trivy.json をアーティファクト/Issue 作成に使う sign: needs: scan runs-on: ubuntu-latest steps: - name: Configure AWS credentials (OIDC) uses: aws-actions/configure-aws-credentials@v2 with: role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} aws-region: ap-northeast-1 - name: Sign image using cosign (container, digest-pin recommended) env: IMAGE: ${{ needs.build.outputs.image }} run: | # cosign のコンテナイメージはダイジェスト固定で使うことを推奨 docker run --rm \ -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN -e AWS_REGION=ap-northeast-1 \ ghcr.io/sigstore/cosign/cosign:v2.12.0 \ sign --key "awskms://${{ secrets.AWS_KMS_KEY_ID }}" ${IMAGE} |
上のテンプレートは最小構成です。環境に応じて以下を追加してください。
- Action のバージョン固定(可能ならコミット SHA)
- GHCR に push できない場合の PAT(スコープは最小限に)
- cosign の検証(署名/チェックサム検査)やコンテナイメージの digest 固定
参考ドキュメント(優先度:公式→補助資料)
公式ドキュメントを優先して参照してください。外部記事(Zenn/Qiita 等)は補助資料として扱い、実装前に公式の仕様とリリースノートを必ず確認してください。
-
GitHub Actions: Workflows と Permissions(公式ドキュメント)
https://docs.github.com/actions -
GitHub OIDC 設定(公式)
https://docs.github.com/actions/deployment/security-hardening-your-deployments/configuring-openid-connect -
Docker + GitHub Actions(公式)
https://docs.docker.com/ci-cd/github-actions/ -
sigstore / cosign リリースページ(署名・チェックサムを確認する)
https://github.com/sigstore/cosign/releases -
Trivy ドキュメント(公式)
https://aquasecurity.github.io/trivy/latest/
外部記事(補助): Zenn / Qiita 等は参考になりますが、情報の鮮度や実装詳細は公式ドキュメントやリリースノートを優先してください。
まとめ
CI 側でビルド・テスト・SBOM・スキャン・署名を完結させ、CD は署名済みの不変イメージをデプロイする責務分離が運用負荷を下げます。実務上は Dockerfile の HEALTHCHECK をランタイムに不要なツールを入れずに実装する、cosign は署名とチェックサムを検証して導入する、Buildx と actions/cache はハッシュベースのキー設計と無効化手順を準備する、脆弱性は自動でチケット化して対応フローに組み込むことが重要です。