GitHub ActionsでCI/CDパイプラインを構築:デプロイを今すぐ自動化する

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

クイックスタート:5分で作るはじめてのGitHub Actionsワークフロー

コードをプッシュした後、サーバーにSSHで手動ログインしてデプロイしたことがある人なら、あの苦労はよくわかるはずだ。GitHub Actionsがその問題を解決してくれたのは約2年前のこと。それ以来、手動デプロイのミスで深夜に予期せぬロールバックをすることはなくなった。

基本的な仕組みはシンプルだ。YAMLファイルを書いてリポジトリにコミットするだけで、プッシュ、プルリクエスト、タグなど何かが起きるたびにGitHubがスクリプトを自動実行してくれる。外部CIサーバーも、Jenkinsのセットアップも不要。追加費用もかからない(必要な場合を除いて)。

始めるのに必要なコマンドはたった2つ:

# ワークフローディレクトリを作成する
mkdir -p .github/workflows
touch .github/workflows/ci.yml

この最小限のパイプラインを貼り付けよう:

name: CIパイプライン

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: コードをチェックアウト
        uses: actions/checkout@v4

      - name: Pythonのセットアップ
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: 依存関係のインストール
        run: pip install -r requirements.txt

      - name: テストの実行
        run: pytest tests/

これをコミットしてGitHubにプッシュし、リポジトリのActionsタブを確認しよう。ワークフローが実行されているのが見えるはずだ。これがあなたの最初のCIパイプラインだ。

詳細解説:ワークフロー構造を理解する

YAML構造は見た目よりも直感的だ。4つの概念を理解すれば、日常的に出会う場面の90%に対応できる。

トリガー(on:

on:ブロックはワークフローを起動するイベントを定義する。よく使うトリガーはこちら:

on:
  push:
    branches: [main]
    tags:
      - 'v*'          # v1.0.0のようなバージョンタグで起動
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'   # 毎日UTC午前2時に実行
  workflow_dispatch:       # GitHub UIから手動でトリガーできる

workflow_dispatchトリガーは過小評価されている。GitHub UI上のボタンをクリックするだけで手動デプロイを実行できる。完全自動化が常に望ましいとは限らないステージング環境では特に重宝する。

ジョブとステップ

ジョブはデフォルトで並列実行される。ジョブ内のステップは順次実行される。ジョブを順番に実行したい場合はneeds:を使う:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pytest tests/

  build:
    runs-on: ubuntu-latest
    needs: test    # 'test'が成功した場合のみ実行
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t myapp:latest .

  deploy:
    runs-on: ubuntu-latest
    needs: build   # 'build'の成功を待ってから実行
    steps:
      - run: echo "デプロイ中..."

シークレット管理

ワークフローファイルに認証情報をハードコードしてはいけない。リポジトリのSettings → Secrets and variables → Actionsからシークレットを追加し、以下のように参照する:

- name: サーバーにデプロイ
  env:
    SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
    SERVER_HOST: ${{ secrets.SERVER_HOST }}
  run: |
    echo "$SSH_KEY" > /tmp/deploy_key
    chmod 600 /tmp/deploy_key
    ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no \
      ubuntu@$SERVER_HOST "cd /app && git pull && systemctl restart myapp"

応用編:本番環境で実際に機能するパターン

複数環境向けマトリックスビルド

Python 3.9、3.10、3.11でコードが動作するか確認したいとき、3つの別々のジョブを書く必要はない。マトリックス戦略がそれを処理してくれる:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ['3.9', '3.10', '3.11']
        os: [ubuntu-latest, windows-latest]

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - run: pytest tests/

これで6つの並列ジョブが作成される。すべてのOSですべてのPythonバージョンをテストする。何か壊れたとき、GitHubはどの組み合わせが失敗したかを正確に教えてくれる。3.9が通るのにWindows上の3.11がクラッシュしている?1回の実行でわかる。

依存関係のキャッシュ

毎回クリーンインストールするとパイプラインが遅くなる。実行間でpipパッケージをキャッシュしよう:

- name: pipパッケージのキャッシュ
  uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

- name: 依存関係のインストール
  run: pip install -r requirements.txt

キャッシュキーはrequirements.txtをハッシュ化しているので、依存関係が変わると自動的に無効化される。中規模のPythonプロジェクトでは、インストール時間が約60秒から約5秒に短縮される。

DockerビルドとレジストリへのPush

Docker Hub向けのビルド&プッシュワークフローの完全版はこちら:

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Docker Hubにログイン
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}

      - name: ビルドしてプッシュ
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            myuser/myapp:latest
            myuser/myapp:${{ github.sha }}

すべてのビルドにgitコミットSHAがタグ付けされる。ロールバックは簡単になる。本番環境でどのコミットが動いているか常に正確にわかるからだ。

環境ベースのデプロイ

GitHub Environmentsを使うと、環境ごとに保護ルールと別々のシークレットを設定できる(ステージング対本番など):

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - name: ステージングにデプロイ
        run: ./deploy.sh staging

  deploy-production:
    runs-on: ubuntu-latest
    environment: production   # 手動承認を要求できる
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    steps:
      - name: 本番環境にデプロイ
        run: ./deploy.sh production

本番環境にレビュアーを必須設定にすれば、誰かがGitHub UI上でApproveをクリックするまで何もリリースされない。手動ゲートは1つ。それ以外はすべて自動化されたまま。

実際のパイプラインから学ぶ実践的なヒント

ワークフローを集中させる

よくあるミスはすべてを1つのワークフローファイルに詰め込むことだ。関心事を分離しよう。毎回のプッシュでテストを実行するci.ymlと、タグまたはmainへのマージ時のみ起動するdeploy.ymlに分ける。ファイルが小さいほど、夜11時に何か壊れたときのデバッグが楽になる。

if:条件を使ってステップをスマートにスキップする

- name: mainブランチのみデプロイ
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  run: ./deploy.sh

テストはすべてのブランチで実行される。デプロイはmainでのみ起動する。フィーチャーブランチが誤って本番環境に触れることはない。

早期失敗、ただしやりすぎない

strategy:
  fail-fast: false   # 1つ失敗しても全マトリックスジョブを完了させる
  matrix:
    python-version: ['3.9', '3.10', '3.11']

デフォルトでは1つのジョブが失敗した瞬間、すべてのマトリックスジョブが停止する。fail-fast: falseを設定すると全体像が見える。3.9は通るのに3.11が壊れているかもしれない。データが多いほど、修正も速くなる。

READMEにステータスバッジを追加する

GitHubはワークフローのステータスバッジを自動生成する。Actionsタブ → ワークフロー → 三点メニュー → Create status badgeから確認できる。READMEに貼り付けよう:

![CIステータス](https://github.com/youruser/yourrepo/actions/workflows/ci.yml/badge.svg)

ランナー使用時間に注意する

フリープランではプライベートリポジトリに月2,000分が付与される。パブリックリポジトリは無制限。大規模なマトリックスビルドでクォータを消費している場合、2つの方法が効果的だ。キャッシュの改善と、ドキュメントの変更でフルテストが起動しないためのpaths:フィルターだ:

on:
  push:
    paths:
      - 'src/**'
      - 'tests/**'
      - 'requirements.txt'
    # docs/やREADME.mdの変更はパイプラインを起動しない

GitHub Actionsは、かつて15ステップの手動デプロイチェックリストだったものをプッシュ1回に凝縮してくれた。学習曲線は見た目ほど急ではない。YAML構造が頭に入れば、あとは自然についてくる。パイプラインがリポジトリ内に存在するということは、コードと一緒にバージョン管理されるということでもある。まずは上のクイックスタートから始め、必要に応じて応用パターンを組み込んでいこう。

Share: