T-CREATOR

GitHub Actions で PostgreSQL/Redis を services で立ち上げるテスト基盤レシピ

GitHub Actions で PostgreSQL/Redis を services で立ち上げるテスト基盤レシピ

CI/CD パイプラインでデータベースやキャッシュサーバーを使ったテストを実行したいとき、環境構築に悩んだことはありませんか。GitHub Actions には services という強力な機能があり、PostgreSQL や Redis といったミドルウェアをコンテナとして簡単に起動できます。

この記事では、GitHub Actions の services 機能を使って PostgreSQL と Redis を立ち上げ、実際のアプリケーションテストを実行する方法を段階的に解説します。初めて CI 環境でデータベースを扱う方でも、すぐに実践できる内容になっていますよ。

背景

CI/CD におけるテスト環境の課題

現代の Web アプリケーション開発では、コードの品質を保つために自動テストが欠かせません。しかし、ローカル環境では動作するテストが、CI 環境では失敗してしまうケースがあります。

特にデータベースやキャッシュサーバーに依存するテストでは、CI 環境にも同じミドルウェアを用意する必要があるのです。

GitHub Actions の services 機能とは

GitHub Actions には services という機能があり、ワークフロー実行時に Docker コンテナとしてミドルウェアを起動できます。これにより、PostgreSQL や Redis などを簡単にテスト環境へ組み込めるようになりました。

services で起動したコンテナは、ジョブの実行中だけ存在し、終了後は自動的にクリーンアップされます。環境を汚さず、毎回クリーンな状態でテストできるのが大きな魅力ですね。

以下の図は、GitHub Actions のワークフロー実行時に services がどのように動作するかを示しています。

mermaidflowchart TB
  trigger["ワークフローのトリガー<br/>(push/PR)"] -->|開始| setup["services コンテナ起動<br/>(PostgreSQL/Redis)"]
  setup -->|準備完了| job["ジョブ実行<br/>(テストコード)"]
  job -->|接続| postgres[("PostgreSQL<br/>コンテナ")]
  job -->|接続| redis[("Redis<br/>コンテナ")]
  job -->|完了| cleanup["コンテナ自動削除"]
  cleanup --> finish["ワークフロー終了"]

図で理解できる要点

  • ワークフロー開始時に services コンテナが自動的に起動されます
  • ジョブ内のテストコードは、起動済みのコンテナへ直接接続できます
  • ジョブ完了後、コンテナは自動的にクリーンアップされます

Docker Hub との連携

GitHub Actions の services は、Docker Hub 上の公式イメージを指定するだけで利用できます。PostgreSQL なら postgres イメージ、Redis なら redis イメージを使用することで、複雑な設定なしにすぐ動かせるのです。

課題

テスト環境構築の複雑さ

従来の CI 環境では、データベースをセットアップするために以下のような手順が必要でした。

  • パッケージマネージャーでミドルウェアをインストール
  • 起動スクリプトを実行し、サービスを開始
  • 接続待機のためのヘルスチェック実装
  • テスト用データベースやユーザーの作成
  • 環境変数や設定ファイルの準備

これらの手順は複雑で、メンテナンスコストも高くなりがちです。

バージョン管理とポータビリティの問題

CI 環境にインストールされているミドルウェアのバージョンが古かったり、ローカル環境と異なったりすることもあります。また、CI サービスを移行する際には、すべての環境構築手順を書き直す必要がありました。

以下の図は、従来の課題と services による解決策を対比しています。

mermaidflowchart LR
  subgraph before["従来の方法"]
    direction TB
    install1["パッケージ<br/>インストール"]
    install1 --> config1["設定ファイル<br/>作成"]
    config1 --> start1["サービス起動"]
    start1 --> wait1["ヘルスチェック<br/>待機"]
    wait1 --> init1["初期化<br/>スクリプト"]
  end

  subgraph after["services による解決"]
    direction TB
    define["services 定義<br/>(YAML)"]
    define --> auto["自動起動/<br/>ヘルスチェック"]
    auto --> ready["すぐ利用可能"]
  end

  before -.->|簡素化| after

図で理解できる要点

  • 従来は複数のステップが必要だった環境構築が、services では YAML 定義だけで完結します
  • ヘルスチェックや起動待機も自動化されます
  • ポータブルな設定で、他の CI 環境への移行も容易になります

接続情報の管理

データベースやキャッシュサーバーへの接続には、ホスト名、ポート番号、認証情報などが必要です。これらを適切に管理し、テストコードから参照できるようにする必要がありました。

解決策

services 機能による統一的な定義

GitHub Actions の services を使えば、ワークフローファイル(YAML)にコンテナの定義を記述するだけで、必要なミドルウェアが自動的に起動されます。

services の主な利点は以下の通りです。

  • Docker コンテナとして起動されるため、環境の再現性が高い
  • ヘルスチェックが自動的に実行され、準備完了まで待機してくれる
  • ジョブ終了後は自動的にクリーンアップされる
  • 複数のサービスを同時に起動できる

ネットワーキングの自動設定

services で起動されたコンテナは、GitHub Actions ランナーから自動的にアクセス可能になります。サービス名がそのままホスト名として使えるため、接続設定も簡単です。

例えば、postgres という名前で定義した PostgreSQL サービスには、localhost:5432(Linux ランナーの場合)や postgres:5432(Docker コンテナジョブの場合)で接続できます。

環境変数による設定の注入

PostgreSQL や Redis の初期設定(パスワード、データベース名など)は、services 定義内の env セクションで環境変数として渡せます。これにより、コンテナ起動時に自動的に設定が適用されるのです。

以下の図は、services を使ったテスト実行の全体フローを示しています。

mermaidsequenceDiagram
  participant GHA as GitHub Actions
  participant PSrv as PostgreSQL<br/>Service
  participant RSrv as Redis<br/>Service
  participant Job as テストジョブ

  GHA->>PSrv: コンテナ起動<br/>(env 設定適用)
  GHA->>RSrv: コンテナ起動<br/>(env 設定適用)
  PSrv->>GHA: ヘルスチェック<br/>OK
  RSrv->>GHA: ヘルスチェック<br/>OK
  GHA->>Job: ジョブ開始
  Job->>PSrv: DB 接続<br/>(localhost:5432)
  Job->>RSrv: キャッシュ接続<br/>(localhost:6379)
  Job->>Job: テスト実行
  Job->>GHA: ジョブ完了
  GHA->>PSrv: コンテナ停止/削除
  GHA->>RSrv: コンテナ停止/削除

図で理解できる要点

  • services コンテナは環境変数を受け取って自動的に初期化されます
  • ヘルスチェックによって、サービスが準備完了してからジョブが開始されます
  • テストジョブは localhost 経由で各サービスへアクセスできます

具体例

基本的な PostgreSQL サービスの定義

まずは、PostgreSQL だけを起動する最小限の設定から始めましょう。

ワークフローファイルの作成

.github​/​workflows ディレクトリ内に、YAML ファイルを作成します。

yamlname: PostgreSQL テスト

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

ワークフローのトリガーを定義しました。main ブランチへの push や PR 作成時に実行されます。

ジョブと services の定義

次に、ジョブと PostgreSQL サービスを定義します。

yamljobs:
  test:
    runs-on: ubuntu-latest

Ubuntu の最新版ランナーを使用します。Linux ランナーでは、services は localhost 経由でアクセスできますよ。

yamlservices:
  postgres:
    image: postgres:15
    env:
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
      POSTGRES_DB: testdb

services セクションで PostgreSQL コンテナを定義しました。postgres:15 という Docker イメージを使い、環境変数で初期設定を指定しています。

  • POSTGRES_USER: データベースユーザー名
  • POSTGRES_PASSWORD: パスワード
  • POSTGRES_DB: 作成するデータベース名

ポートマッピングとヘルスチェック

yamlports:
  - 5432:5432
options: >-
  --health-cmd pg_isready
  --health-interval 10s
  --health-timeout 5s
  --health-retries 5

ports でポートマッピングを指定し、ホストマシンの 5432 ポートをコンテナの 5432 ポートにマッピングしています。

options では、ヘルスチェックの設定を記述しました。

  • --health-cmd: ヘルスチェックコマンド(pg_isready で PostgreSQL の準備状態を確認)
  • --health-interval: チェック間隔(10 秒ごと)
  • --health-timeout: タイムアウト時間(5 秒)
  • --health-retries: リトライ回数(5 回)

これにより、PostgreSQL が完全に起動してからテストが開始されます。

Redis サービスの追加

PostgreSQL に加えて、Redis も同時に起動してみましょう。

Redis サービスの定義

同じ services セクション内に Redis を追加します。

yamlservices:
  postgres:
    # ... (前述の PostgreSQL 設定)

  redis:
    image: redis:7

Redis の公式イメージ(バージョン 7)を使用します。Redis は認証なしでもデフォルトで動作するため、環境変数の設定は最小限で済みますね。

Redis のポートマッピングとヘルスチェック

yamlports:
  - 6379:6379
options: >-
  --health-cmd "redis-cli ping"
  --health-interval 10s
  --health-timeout 5s
  --health-retries 5

Redis のデフォルトポート 6379 をマッピングし、redis-cli ping コマンドでヘルスチェックを行います。

テストステップの実装

ここからは、実際にサービスへ接続してテストを実行するステップを定義します。

チェックアウトと依存関係のインストール

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

  - name: Node.js のセットアップ
    uses: actions/setup-node@v4
    with:
      node-version: '20'

リポジトリのコードをチェックアウトし、Node.js 20 をセットアップします。

yaml- name: 依存関係のインストール
  run: yarn install --frozen-lockfile

Yarn を使って依存パッケージをインストールしました。--frozen-lockfile オプションで、yarn.lock ファイルの変更を防ぎます。

環境変数の設定

テストコードから接続情報を参照できるよう、環境変数を設定します。

yaml- name: テスト実行
  env:
    DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
    REDIS_URL: redis://localhost:6379
  run: yarn test

DATABASE_URLREDIS_URL を環境変数として定義し、テストコマンド yarn test を実行します。

テストコード内では、これらの環境変数を参照して接続するだけで、PostgreSQL と Redis を使ったテストが実行できますよ。

実際の接続テストコード例

Node.js/TypeScript でのテストコード例を示します。

PostgreSQL 接続のテスト

まず、PostgreSQL へ接続するテストコードです。

typescriptimport { Client } from 'pg';

describe('PostgreSQL 接続テスト', () => {
  let client: Client;

  beforeAll(async () => {
    // 環境変数から接続情報を取得
    client = new Client({
      connectionString: process.env.DATABASE_URL,
    });

pg パッケージの Client を使い、環境変数 DATABASE_URL から接続情報を取得しています。

typescript    // データベースへ接続
    await client.connect();
  });

  afterAll(async () => {
    // テスト終了後に接続を閉じる
    await client.end();
  });

beforeAll で接続を確立し、afterAll でクリーンアップを行います。

typescript  test('テーブルの作成と挿入', async () => {
    // テーブル作成
    await client.query(`
      CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        name VARCHAR(100) NOT NULL
      )
    `);

簡単なテーブルを作成しました。SERIAL 型で自動採番される主キーを定義しています。

typescript// データ挿入
const insertResult = await client.query(
  'INSERT INTO users (name) VALUES ($1) RETURNING *',
  ['テストユーザー']
);

// 挿入されたデータを検証
expect(insertResult.rows[0].name).toBe('テストユーザー');

プレースホルダー $1 を使ってデータを安全に挿入し、戻り値を検証しています。

typescript    // データ取得
    const selectResult = await client.query('SELECT * FROM users');

    expect(selectResult.rows.length).toBeGreaterThan(0);
  });
});

挿入したデータが正しく取得できることを確認しました。これで PostgreSQL への接続が正常に動作していることが検証できます。

Redis 接続のテスト

次に、Redis への接続をテストするコードです。

typescriptimport { createClient, RedisClientType } from 'redis';

describe('Redis 接続テスト', () => {
  let redisClient: RedisClientType;

  beforeAll(async () => {
    // 環境変数から接続情報を取得
    redisClient = createClient({
      url: process.env.REDIS_URL,
    });

redis パッケージの createClient を使い、環境変数から Redis の URL を取得しています。

typescript    // Redis へ接続
    await redisClient.connect();
  });

  afterAll(async () => {
    // 接続を閉じる
    await redisClient.quit();
  });

connect で接続し、テスト終了時には quit で切断します。

typescript  test('キャッシュの保存と取得', async () => {
    const key = 'test:key';
    const value = 'テスト値';

    // 値を保存
    await redisClient.set(key, value);

Redis に文字列データを保存しました。キーには test: というプレフィックスを付けて名前空間を分けています。

typescript// 値を取得
const retrieved = await redisClient.get(key);

expect(retrieved).toBe(value);

保存した値が正しく取得できることを確認しています。

typescript    // 有効期限付きでデータを保存
    await redisClient.setEx('test:expiring', 60, 'この値は60秒で期限切れ');

    const expiringValue = await redisClient.get('test:expiring');
    expect(expiringValue).toBe('この値は60秒で期限切れ');
  });
});

setEx を使って、TTL(生存期間)付きでデータを保存しました。キャッシュの有効期限設定が正しく動作することを確認できます。

完全なワークフローファイル

これまでの内容をまとめた、完全なワークフローファイルをご紹介します。

yamlname: PostgreSQL  Redis を使ったテスト

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

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      # PostgreSQL サービス
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

      # Redis サービス
      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

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

      - name: Node.js のセットアップ
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'

      - name: 依存関係のインストール
        run: yarn install --frozen-lockfile

      - name: テスト実行
        env:
          DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
          REDIS_URL: redis://localhost:6379
          NODE_ENV: test
        run: yarn test

      - name: カバレッジレポートのアップロード
        uses: codecov/codecov-action@v3
        if: always()
        with:
          files: ./coverage/coverage-final.json

このワークフローでは、以下の機能が実装されています。

#機能説明
1マルチブランチ対応main と develop ブランチへの push/PR で実行
2複数サービス起動PostgreSQL と Redis を同時起動
3ヘルスチェック各サービスの準備完了を自動確認
4キャッシュ活用Node.js のキャッシュで依存関係インストールを高速化
5カバレッジレポートテスト結果を Codecov へアップロード

パフォーマンス最適化のポイント

実際の運用では、以下のような最適化も検討できます。

イメージのバージョン固定

yamlservices:
  postgres:
    image: postgres:15.3 # メジャー・マイナー・パッチまで固定

バージョンを詳細に指定することで、予期しない動作変更を防げます。ただし、セキュリティパッチが適用されなくなるため、定期的な更新が必要ですね。

不要なログの抑制

yamlservices:
  postgres:
    # ... (他の設定)
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5
      -c log_statement=none

-c log_statement=none オプションで、PostgreSQL の詳細なログ出力を抑制できます。ログ量が減ることで、ワークフローの実行速度がわずかに向上します。

Redis の永続化無効化

yamlservices:
  redis:
    # ... (他の設定)
    options: >-
      --health-cmd "redis-cli ping"
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5
      --save ""
      --appendonly no

テスト環境では永続化が不要なため、--save ""--appendonly no で無効化します。これにより、ディスク I/O が減少し、パフォーマンスが向上するのです。

エラーハンドリングと診断

テストが失敗した場合の診断方法も押さえておきましょう。

サービスログの確認

yaml- name: PostgreSQL ログの出力(エラー時)
  if: failure()
  run: |
    docker logs $(docker ps -q --filter "ancestor=postgres:15")

テスト失敗時(if: failure())に、PostgreSQL コンテナのログを出力します。接続エラーや初期化エラーの原因を特定しやすくなりますよ。

接続テストの追加

yaml- name: PostgreSQL 接続確認
  run: |
    sudo apt-get install -y postgresql-client
    PGPASSWORD=testpass psql -h localhost -U testuser -d testdb -c "SELECT version();"

本格的なテストの前に、psql コマンドで接続できるか確認するステップを追加しました。これにより、サービス起動の問題とアプリケーションコードの問題を切り分けられます。

Redis 接続確認

yaml- name: Redis 接続確認
  run: |
    redis-cli -h localhost ping

redis-cli で Redis への接続を確認します。PONG が返ってくれば、Redis が正常に動作していることがわかりますね。

マトリックス戦略による複数バージョンテスト

異なるバージョンの PostgreSQL や Redis でテストしたい場合は、マトリックス戦略が便利です。

yamljobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        postgres-version: [13, 14, 15]
        redis-version: [6, 7]

複数のバージョンの組み合わせでテストを実行する設定です。

yamlservices:
  postgres:
    image: postgres:${{ matrix.postgres-version }}
    # ... (他の設定)

  redis:
    image: redis:${{ matrix.redis-version }}
    # ... (他の設定)

${{ matrix.postgres-version }} のように、マトリックス変数を使ってイメージのバージョンを動的に指定できます。

この設定により、PostgreSQL の 3 バージョン × Redis の 2 バージョン = 合計 6 パターンの組み合わせでテストが自動実行されます。互換性の問題を早期に発見できるのは心強いですね。

Docker コンテナジョブでの使用

ジョブ自体を Docker コンテナ内で実行する場合は、接続方法が少し異なります。

yamljobs:
  test:
    runs-on: ubuntu-latest

    container:
      image: node:20

ジョブを Node.js 20 のコンテナ内で実行します。

yamlservices:
  postgres:
    image: postgres:15
    env:
      POSTGRES_USER: testuser
      POSTGRES_PASSWORD: testpass
      POSTGRES_DB: testdb
    # ports は不要(コンテナ間通信)
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

コンテナジョブでは、ports の指定は不要です。services コンテナとジョブコンテナは同じ Docker ネットワーク内にあるため、直接通信できます。

yamlsteps:
  - name: テスト実行
    env:
      # localhost ではなくサービス名を使用
      DATABASE_URL: postgresql://testuser:testpass@postgres:5432/testdb
      REDIS_URL: redis://redis:6379
    run: yarn test

接続先のホスト名は localhost ではなく、サービス名postgresredis)を使います。Docker ネットワークの DNS によって、サービス名が自動的に解決されるのです。

まとめ

GitHub Actions の services 機能を使えば、PostgreSQL や Redis といったミドルウェアを簡単にテスト環境へ組み込めます。

本記事でご紹介した内容をまとめると、以下のようになります。

押さえておきたいポイント

#ポイント説明
1services の定義ワークフロー YAML に Docker イメージとポート、環境変数を記述
2ヘルスチェックoptions でヘルスチェックコマンドを指定し、準備完了を自動確認
3接続方法Linux ランナーでは localhost、コンテナジョブではサービス名を使用
4環境変数env セクションで初期設定を注入し、テストステップで接続情報を参照
5複数サービスPostgreSQL と Redis を同時に起動し、統合的なテストを実行可能

services を使うメリット

  • シンプルな設定:YAML ファイルだけで完結し、複雑なセットアップスクリプトが不要です
  • 再現性:Docker イメージを使うため、ローカル環境と CI 環境の差異が最小化されます
  • 自動クリーンアップ:ジョブ終了後、コンテナは自動的に削除され、環境が汚れません
  • 並列実行:マトリックス戦略と組み合わせて、複数バージョンでの互換性テストも簡単です

今後の活用に向けて

本記事で紹介した設定をベースに、以下のような拡張も検討できます。

  • MySQL や MariaDB: PostgreSQL の代わりに MySQL を使う
  • MongoDB: NoSQL データベースのテスト環境を構築
  • Elasticsearch: 全文検索機能のテスト
  • RabbitMQ や Kafka: メッセージキューを使った非同期処理のテスト

GitHub Actions の services は、これらすべてに対応できる柔軟な機能です。ぜひ、あなたのプロジェクトでも活用してみてください。

データベースやキャッシュサーバーを含む統合テストが CI で自動実行できるようになれば、コードの品質と開発速度の両方を大きく向上させられますよ。

関連リンク