はじめに
「手動デプロイ、もうやめませんか?」
GitHub Actionsを使えば、コードのプッシュをトリガーにテスト、ビルド、デプロイを完全自動化できます。この記事では、GitHub Actionsの基礎から実践的なワークフロー構築まで、詳しく解説します。
ただし 実務で本当に効くのは
- どの粒度でジョブを分けるか
- secretsや権限をどう扱うか
- 失敗時に原因が追える設計にするか
といった 設計と運用の部分です。
ここを押さえないと
ワークフローは動くのに 運用が辛い CI が出来上がります。
この記事で最初に固める指針
指針 速い フィードバックを最優先にする
CIは 開発者にとっての待ち時間 そのものです。
次の順で速さを作ります。
- 依存関係のキャッシュ
- テストを分割して並列化
- 変更のある部分だけ回す paths 条件
指針 権限は最小にする
Actionsは便利ですが
CIはしばしば 外部から実行されるコードを動かす場 になります。
次を意識すると事故りにくいです。
- secretsは必要なジョブにだけ渡す
- permissionsを明示して最小にする
- pull_requestとpull_request_targetの違いを理解する
指針 失敗時に直せるログを残す
- どのステップが失敗したかが一目で分かる命名
- 重要な値はマスクしながら出す
- 可能なら成果物やテストレポートをアップロード
この3つを先に設計すると
後からワークフローが増えても破綻しにくいです。
どこから作るかの最短ルート
CI/CDはやりたいことが多すぎて迷いがちなので、最初に順番を固定します。
- 変更が入ったらまずテストだけ確実に回す(品質ゲート)
- 次にビルドと成果物を作る(再現性)
- 最後にデプロイ(権限と承認を強くする)
本番の事故は「テスト不足」より「権限と秘密情報」と「失敗時に直せない」から起きることが多いです。
最初から運用前提で組むほど、長期的に楽になります。
まず決める設計判断の軸
どのイベントで何を走らせるか
- pull_request: 速い検証のみ(lint、unit、型、軽い結合)
- push to main: 本番相当の検証と成果物作成
- schedule: 依存関係スキャンや重いE2Eなど
Secretsを渡すか 渡さないか
secretsを必要とするジョブは「信頼できるコードだけが実行される」前提が必要です。
- 外部PRでは secrets を使わない
- どうしても必要なら環境分離や手動承認を挟む
- OIDCで短命トークンを発行し、長期鍵を置かない
どこをキャッシュして どこをキャッシュしないか
キャッシュは速くなりますが「壊れたキャッシュ」が原因不明の失敗を生みます。
- 依存関係(npm、pip等)はキャッシュする価値が高い
- ビルド成果物のキャッシュはキー設計が重要(lockfileやOS、Node版)
事故防止チェックリスト(最小版)
- permissionsを明示し、必要最小限になっている
- secretsは必要なジョブにだけ渡している
- pull_request_target を安易に使っていない
- 失敗時に調査できるログと成果物(テストレポート等)が残る
- キャッシュキーに lockfile とランタイムのバージョンが含まれる
GitHub Actionsの基本
基本用語
| 用語 | 説明 |
|---|---|
| Workflow | 自動化されたプロセス全体(YAMLファイルで定義) |
| Event | ワークフローをトリガーするイベント(push, pull_request等) |
| Job | 同じランナーで実行されるステップの集まり |
| Step | 個々のタスク(コマンド実行やAction使用) |
| Action | 再利用可能なワークフローの部品 |
| Runner | ワークフローを実行するサーバー |
ワークフローファイルの基本構造
# .github/workflows/ci.yml
name: CI Pipeline # ワークフロー名
on: # トリガー
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs: # ジョブ定義
build: # ジョブ名
runs-on: ubuntu-latest # 実行環境
steps: # ステップ
- name: Checkout code
uses: actions/checkout@v4
- name: Run tests
run: npm test
トリガーイベント
プッシュ・プルリクエスト
on:
push:
branches:
- main
- 'release/**' # release/で始まるブランチ
paths:
- 'src/**' # src/配下の変更時のみ
- '!src/**/*.md' # ただしMDファイルは除外
tags:
- 'v*' # vで始まるタグ
pull_request:
types: [opened, synchronize, reopened]
branches:
- main
スケジュール実行
on:
schedule:
# 毎日午前9時(UTC)に実行
- cron: '0 9 * * *'
# 毎週月曜の午前0時に実行
- cron: '0 0 * * 1'
手動実行
on:
workflow_dispatch:
inputs:
environment:
description: 'デプロイ先環境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
debug:
description: 'デバッグモード'
required: false
type: boolean
default: false
他のワークフローからの呼び出し
on:
workflow_call:
inputs:
node-version:
required: true
type: string
secrets:
npm-token:
required: true
ジョブとステップ
基本的なジョブ構成
jobs:
test:
runs-on: ubuntu-latest
steps:
- 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: Run linter
run: npm run lint
- name: Run tests
run: npm test
マトリックス戦略
複数の環境・バージョンでテストを実行できます。
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
exclude:
- os: windows-latest
node-version: 18
include:
- os: ubuntu-latest
node-version: 20
experimental: true
fail-fast: false # 1つ失敗しても他は続行
max-parallel: 3 # 同時実行数
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
ジョブの依存関係
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
build:
needs: test # testジョブ完了後に実行
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
deploy:
needs: [test, build] # 複数ジョブの完了を待つ
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
条件付き実行
jobs:
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy to Production
if: success()
run: ./deploy.sh production
- name: Notify on failure
if: failure()
run: ./notify-failure.sh
- name: Cleanup
if: always() # 成功・失敗に関わらず実行
run: ./cleanup.sh
シークレットと環境変数
シークレットの使用
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to AWS
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 sync ./dist s3://${{ secrets.S3_BUCKET }}
環境変数
env: # ワークフロー全体の環境変数
NODE_ENV: production
jobs:
build:
runs-on: ubuntu-latest
env: # ジョブ固有の環境変数
BUILD_TARGET: web
steps:
- name: Build
env: # ステップ固有の環境変数
API_URL: https://api.example.com
run: npm run build
Environments(環境)
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh staging
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh production
アーティファクトとキャッシュ
アーティファクトの保存と共有
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy
run: ./deploy.sh
キャッシュの活用
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # 自動的にnode_modulesをキャッシュ
- name: Install dependencies
run: npm ci
カスタムキャッシュ:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
~/.npm
node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm ci
実践的なワークフロー例
Node.js プロジェクトのCI
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- 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: Lint
run: npm run lint
- name: Type check
run: npm run type-check
- name: Test
run: npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
build:
needs: lint-and-test
runs-on: ubuntu-latest
steps:
- 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: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build
path: dist/
Docker イメージのビルドとプッシュ
# .github/workflows/docker.yml
name: Docker Build and Push
on:
push:
branches: [main]
tags: ['v*']
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKERHUB_USERNAME }}/my-app
ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
AWS へのデプロイ
# .github/workflows/deploy-aws.yml
name: Deploy to AWS
on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Environment'
required: true
type: choice
options:
- staging
- production
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment || 'staging' }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push Docker image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: my-app
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Deploy to ECS
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: my-app
IMAGE_TAG: ${{ github.sha }}
run: |
aws ecs update-service \
--cluster my-cluster \
--service my-service \
--force-new-deployment
Vercel へのデプロイ
# .github/workflows/deploy-vercel.yml
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Vercel CLI
run: npm install -g vercel@latest
- name: Pull Vercel Environment
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy to Vercel
id: deploy
run: |
url=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})
echo "url=$url" >> $GITHUB_OUTPUT
- name: Comment PR with preview URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 Preview deployed to: ${{ steps.deploy.outputs.url }}`
})
Terraform によるインフラ管理
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
paths: ['terraform/**']
pull_request:
branches: [main]
paths: ['terraform/**']
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform
steps:
- uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Terraform Format
run: terraform fmt -check
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
continue-on-error: true
- name: Comment PR with plan
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const output = `#### Terraform Plan 📖
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\``;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
});
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
再利用可能なワークフロー
Composite Action
# .github/actions/setup-node-and-install/action.yml
name: Setup Node.js and Install Dependencies
description: Sets up Node.js and installs npm dependencies
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
runs:
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
shell: bash
run: npm ci
使用方法:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-node-and-install
with:
node-version: '20'
- run: npm run build
Reusable Workflow
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy Workflow
on:
workflow_call:
inputs:
environment:
required: true
type: string
app-name:
required: true
type: string
secrets:
aws-access-key-id:
required: true
aws-secret-access-key:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.aws-access-key-id }}
aws-secret-access-key: ${{ secrets.aws-secret-access-key }}
aws-region: ap-northeast-1
- name: Deploy
run: |
echo "Deploying ${{ inputs.app-name }} to ${{ inputs.environment }}"
# デプロイ処理
呼び出し側:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
app-name: my-app
secrets:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
deploy-production:
needs: deploy-staging
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
app-name: my-app
secrets:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
セキュリティベストプラクティス
シークレットの保護
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 悪い例:シークレットをログに出力
# - run: echo ${{ secrets.API_KEY }}
# 良い例:環境変数として渡す
- name: Use secret safely
env:
API_KEY: ${{ secrets.API_KEY }}
run: ./script.sh # スクリプト内で$API_KEYを使用
Pull Request のセキュリティ
on:
pull_request_target: # フォークからのPRでもシークレットにアクセス可能
types: [labeled]
jobs:
build:
# 信頼できるラベルがある場合のみ実行
if: contains(github.event.pull_request.labels.*.name, 'safe-to-test')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
権限の最小化
permissions:
contents: read # コードの読み取りのみ
packages: write # パッケージの書き込み
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
依存関係のピン留め
jobs:
build:
runs-on: ubuntu-latest
steps:
# 悪い例:メジャーバージョンのみ指定
# - uses: actions/checkout@v4
# 良い例:コミットハッシュでピン留め
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
デバッグとトラブルシューティング
デバッグログの有効化
jobs:
debug:
runs-on: ubuntu-latest
steps:
- name: Enable debug logging
run: |
echo "ACTIONS_STEP_DEBUG=true" >> $GITHUB_ENV
echo "ACTIONS_RUNNER_DEBUG=true" >> $GITHUB_ENV
- name: Print context
run: |
echo "github.event_name: ${{ github.event_name }}"
echo "github.ref: ${{ github.ref }}"
echo "github.sha: ${{ github.sha }}"
コンテキストのダンプ
jobs:
dump-context:
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- name: Dump job context
env:
JOB_CONTEXT: ${{ toJson(job) }}
run: echo "$JOB_CONTEXT"
- name: Dump steps context
env:
STEPS_CONTEXT: ${{ toJson(steps) }}
run: echo "$STEPS_CONTEXT"
失敗時の対応
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
id: build
run: npm run build
continue-on-error: true
- name: Upload logs on failure
if: steps.build.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: build-logs
path: logs/
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v1.26.0
with:
channel-id: 'alerts'
slack-message: 'Build failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
- name: Fail if build failed
if: steps.build.outcome == 'failure'
run: exit 1
まとめ
| 機能 | 説明 |
|---|---|
| トリガー | push, pull_request, schedule, workflow_dispatch |
| ジョブ | マトリックス戦略、依存関係、条件付き実行 |
| シークレット | 安全な認証情報の管理 |
| キャッシュ | 依存関係のキャッシュで高速化 |
| アーティファクト | ジョブ間でのファイル共有 |
| 再利用 | Composite Action, Reusable Workflow |
GitHub Actionsを活用して、CI/CDパイプラインを構築し、開発プロセスを自動化しましょう!