12
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

「手動デプロイ、もうやめませんか?」

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パイプラインを構築し、開発プロセスを自動化しましょう!

12
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?