Laravel + React アプリケーションのCI/CDパイプラインと本番環境デプロイ

はじめに

モダンなウェブアプリケーション開発において、継続的インテグレーション(CI)と継続的デリバリー(CD)のパイプラインを構築することは、品質を維持しながら迅速にデプロイを行うために不可欠です。本記事では、Laravel(バックエンド)とReact(フロントエンド)を組み合わせたアプリケーションの CI/CD パイプラインの構築方法と、本番環境へのデプロイ方法について解説します。

具体的には、以下のトピックをカバーします:

  1. CI/CD パイプラインの基本概念
  2. GitHub Actions を使った CI/CD パイプラインの構築
  3. 自動テストの設定
  4. 本番環境へのデプロイ方法(複数の選択肢)
  5. デプロイ後の自動化タスク
  6. セキュリティ対策

CI/CD パイプラインの基本概念

CI(継続的インテグレーション)とは

継続的インテグレーションは、開発者がコードを頻繁にメインリポジトリに統合し、自動的に検証するプラクティスです。CI の主な目的は:

  • コードの変更をできるだけ早く検出して修正する
  • ビルドやテストの自動化で人的ミスを減らす
  • 小さな変更を頻繁に統合することで、マージの問題を減らす

CD(継続的デリバリー/デプロイメント)とは

継続的デリバリーは、アプリケーションを自動的にステージング環境にデプロイし、本番環境へのデプロイを手動で行うプラクティスです。一方、継続的デプロイメントは本番環境へのデプロイも自動化します。CD の主な目的は:

  • デプロイプロセスの自動化で人的ミスを減らす
  • リリースサイクルを短縮する
  • フィードバックループを改善する

GitHub Actions を使った CI/CD パイプラインの構築

今回は一般的な GitHub Actions を使用した CI/CD パイプラインの構築方法を解説します。他の CI/CD ツール(GitLab CI、CircleCI、Jenkins など)でも同様の概念が適用できます。

プロジェクト構造

以下のようなプロジェクト構造を想定します:

project-root/
├── backend/            # Laravel プロジェクト
├── frontend/           # React プロジェクト
├── docker/             # Docker 設定ファイル
├── docker-compose.yml  # Docker Compose 設定
└── .github/            # GitHub Actions ワークフロー
    └── workflows/
        ├── ci-backend.yml
        ├── ci-frontend.yml
        └── deploy.yml

バックエンド(Laravel)の CI ワークフロー

.github/workflows/ci-backend.yml を以下のように作成します:

name: Backend CI

on:
  push:
    branches: [ main, develop ]
    paths:
      - 'backend/**'
  pull_request:
    branches: [ main, develop ]
    paths:
      - 'backend/**'

jobs:
  laravel-tests:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: laravel_test
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
    - uses: actions/checkout@v3
    
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.2'
        extensions: mbstring, dom, fileinfo, mysql
        coverage: xdebug
    
    - name: Copy .env
      run: php -r "file_exists('backend/.env') || copy('backend/.env.example', 'backend/.env');"
    
    - name: Install Composer dependencies
      working-directory: ./backend
      run: composer install --prefer-dist --no-progress
    
    - name: Generate key
      working-directory: ./backend
      run: php artisan key:generate
    
    - name: Configure Database
      working-directory: ./backend
      run: |
        echo "DB_CONNECTION=mysql" >> .env
        echo "DB_HOST=127.0.0.1" >> .env
        echo "DB_PORT=3306" >> .env
        echo "DB_DATABASE=laravel_test" >> .env
        echo "DB_USERNAME=root" >> .env
        echo "DB_PASSWORD=password" >> .env
    
    - name: Run Migrations
      working-directory: ./backend
      run: php artisan migrate
    
    - name: Execute PHPUnit Tests
      working-directory: ./backend
      run: vendor/bin/phpunit
    
    - name: Execute PHP Code Sniffer
      working-directory: ./backend
      run: |
        composer require --dev squizlabs/php_codesniffer
        vendor/bin/phpcs --standard=PSR12 app
    
    - name: Static Analysis with PHPStan
      working-directory: ./backend
      run: |
        composer require --dev phpstan/phpstan
        vendor/bin/phpstan analyse app --level=5

フロントエンド(React)の CI ワークフロー

.github/workflows/ci-frontend.yml を以下のように作成します:

name: Frontend CI

on:
  push:
    branches: [ main, develop ]
    paths:
      - 'frontend/**'
  pull_request:
    branches: [ main, develop ]
    paths:
      - 'frontend/**'

jobs:
  react-tests:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json
    
    - name: Install dependencies
      working-directory: ./frontend
      run: npm ci
    
    - name: Lint check
      working-directory: ./frontend
      run: npm run lint
    
    - name: Run tests
      working-directory: ./frontend
      run: npm test -- --coverage
    
    - name: Build
      working-directory: ./frontend
      run: npm run build

自動テストの設定

Laravel テストの設定

まず、Laravel プロジェクトで適切なテストを作成します。

1. PHPUnit テストの作成

cd backend
php artisan make:test UserControllerTest

backend/tests/Feature/UserControllerTest.php を次のように編集します:

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class UserControllerTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function it_returns_users_list()
    {
        $users = User::factory()->count(3)->create();

        $response = $this->getJson('/api/users');

        $response->assertStatus(200)
                 ->assertJsonCount(3);
    }

    /** @test */
    public function it_can_create_a_user()
    {
        $userData = [
            'name' => 'Test User',
            'email' => 'test@example.com',
            'password' => 'password',
            'password_confirmation' => 'password',
        ];

        $response = $this->postJson('/api/users', $userData);

        $response->assertStatus(201)
                 ->assertJsonFragment(['name' => 'Test User']);

        $this->assertDatabaseHas('users', [
            'name' => 'Test User',
            'email' => 'test@example.com',
        ]);
    }
}

React テストの設定

React のテストも設定します。

1. テストの作成

frontend/src/App.test.tsx を編集します:

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders task list header', () => {
  render(<App />);
  const headerElement = screen.getByText(/タスク一覧/i);
  expect(headerElement).toBeInTheDocument();
});

デプロイ用ワークフロー

.github/workflows/deploy.yml を以下のように作成します:

name: Deploy

on:
  push:
    branches: [ main ]
    
jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    # バックエンドのビルド
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.2'
        extensions: mbstring, dom, fileinfo
    
    - name: Install Composer dependencies
      working-directory: ./backend
      run: composer install --no-dev --prefer-dist --no-progress
    
    # フロントエンドのビルド
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json
    
    - name: Install frontend dependencies
      working-directory: ./frontend
      run: npm ci
    
    - name: Build frontend
      working-directory: ./frontend
      run: npm run build
    
    # デプロイ方法によって異なる部分(例はVPSへのデプロイ)
    - name: Deploy to VPS
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USERNAME }}
        key: ${{ secrets.VPS_SSH_KEY }}
        script: |
          cd /var/www/myapp
          git pull origin main
          cd backend
          composer install --no-dev
          php artisan migrate --force
          php artisan config:cache
          php artisan route:cache
          php artisan view:cache
          cd ../frontend
          cp -r build/* /var/www/myapp/public/

本番環境へのデプロイ方法

以下に、いくつかの一般的なデプロイオプションを紹介します。

1. VPSへのデプロイ

上記のワークフローではVPSへのデプロイ例を示しましたが、より詳細なスクリプトを紹介します。

SSH設定

GitHub Secretsに以下の値を設定します:

  • VPS_HOST – サーバーのIPアドレスまたはホスト名
  • VPS_USERNAME – SSHユーザー名
  • VPS_SSH_KEY – SSH秘密鍵

デプロイスクリプト

#!/bin/bash
# デプロイスクリプト:/var/www/myapp/deploy.sh

set -e

# アプリケーションディレクトリ
APP_DIR="/var/www/myapp"

echo "Deploying application..."

# リポジトリを最新に更新
cd $APP_DIR
git pull origin main

# バックエンドのデプロイ
cd $APP_DIR/backend
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache

# フロントエンドのデプロイ(ビルド済みのファイルをコピー)
cd $APP_DIR/frontend
cp -r build/* $APP_DIR/public/

# キャッシュクリア
php artisan cache:clear

# Webサーバーの設定を再読み込み
sudo systemctl reload nginx

echo "Application deployed successfully!"

Nginx設定

server {
    listen 80;
    server_name myapp.example.com;
    root /var/www/myapp/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

2. Dockerを使ったデプロイ

DockerコンテナとしてアプリケーションをデプロイするCIワークフローの例です。

.github/workflows/deploy-docker.yml:

name: Deploy Docker

on:
  push:
    branches: [ main ]
    
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Login to DockerHub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    
    - name: Build and push backend
      uses: docker/build-push-action@v4
      with:
        context: ./backend
        file: ./docker/php/Dockerfile.prod
        push: true
        tags: yourusername/myapp-backend:latest
    
    - name: Build and push frontend
      uses: docker/build-push-action@v4
      with:
        context: ./frontend
        file: ./docker/node/Dockerfile.prod
        push: true
        tags: yourusername/myapp-frontend:latest
    
    - name: Deploy to server
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USERNAME }}
        key: ${{ secrets.VPS_SSH_KEY }}
        script: |
          cd /var/www/myapp
          docker-compose pull
          docker-compose up -d
          docker exec myapp-backend php artisan migrate --force

本番環境用の docker-compose.yml:

version: '3'
services:
  app:
    image: yourusername/myapp-backend:latest
    container_name: myapp-backend
    restart: unless-stopped
    volumes:
      - ./storage:/var/www/html/storage
      - ./.env:/var/www/html/.env
    networks:
      - app-network
    depends_on:
      - db

  webserver:
    image: nginx:alpine
    container_name: myapp-webserver
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/prod.conf:/etc/nginx/conf.d/default.conf
      - ./ssl:/etc/nginx/ssl
    depends_on:
      - app
    networks:
      - app-network

  db:
    image: mysql:8.0
    container_name: myapp-db
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_USER: ${DB_USERNAME}
    volumes:
      - dbdata:/var/lib/mysql
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  dbdata:

3. クラウドサービスへのデプロイ

AWS Elastic Beanstalkへのデプロイ

.github/workflows/deploy-aws.yml:

name: Deploy to AWS

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    # バックエンドのビルド
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.2'
        extensions: mbstring, dom, fileinfo
    
    - name: Install Composer dependencies
      working-directory: ./backend
      run: composer install --no-dev --prefer-dist --no-progress
    
    # フロントエンドのビルド
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
    
    - name: Install frontend dependencies
      working-directory: ./frontend
      run: npm ci
    
    - name: Build frontend
      working-directory: ./frontend
      run: npm run build
    
    # フロントエンドのビルド結果をバックエンドのpublicディレクトリにコピー
    - name: Copy frontend build to backend
      run: cp -r frontend/build/* backend/public/
    
    # Elastic Beanstalkにデプロイするためのzipを作成
    - name: Generate deployment package
      run: |
        cd backend
        zip -r ../deploy.zip .
    
    # AWS認証情報を設定
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ secrets.AWS_REGION }}
    
    # Elastic Beanstalkにデプロイ
    - name: Deploy to EB
      uses: einaregilsson/beanstalk-deploy@v21
      with:
        aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        application_name: myapp
        environment_name: myapp-production
        region: ${{ secrets.AWS_REGION }}
        version_label: ${{ github.sha }}
        deployment_package: deploy.zip

Herokuへのデプロイ

.github/workflows/deploy-heroku.yml:

name: Deploy to Heroku

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    # フロントエンドのビルド
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
    
    - name: Install frontend dependencies
      working-directory: ./frontend
      run: npm ci
    
    - name: Build frontend
      working-directory: ./frontend
      run: npm run build
    
    # フロントエンドのビルド結果をバックエンドのpublicディレクトリにコピー
    - name: Copy frontend build to backend
      run: cp -r frontend/build/* backend/public/
    
    # Procfileが必要なことを確認
    - name: Check Procfile
      run: |
        if [ ! -f backend/Procfile ]; then
          echo "web: vendor/bin/heroku-php-apache2 public/" > backend/Procfile
        fi
    
    # Herokuにデプロイ
    - name: Deploy to Heroku
      uses: akhileshns/heroku-deploy@v3.12.12
      with:
        heroku_api_key: ${{ secrets.HEROKU_API_KEY }}
        heroku_app_name: ${{ secrets.HEROKU_APP_NAME }}
        heroku_email: ${{ secrets.HEROKU_EMAIL }}
        appdir: backend

デプロイ後の自動化タスク

デプロイ後に実行する重要なタスクがいくつかあります:

1. データベースマイグレーション

php artisan migrate --force

2. キャッシュのクリアと再生成

php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache

3. キューワーカーの再起動

Laravel でキューを使用している場合:

php artisan queue:restart

4. Supervisor の設定(必要な場合)

キューワーカーを管理するための Supervisor の設定例:

[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/myapp/backend/artisan queue:work --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/myapp/backend/storage/logs/worker.log

セキュリティ対策

1. 環境変数の安全な取り扱い

環境変数はGitHubのSecretsやCI/CDプラットフォームのシークレット機能を使用して管理します。決してハードコードしないでください。

例えば、GitHub Secretsを使用して以下のような機密情報を保存します:

  • DB_PASSWORD
  • APP_KEY
  • API_KEYS
  • デプロイ認証情報

2. デプロイ前のセキュリティスキャン

CI/CDパイプラインにセキュリティスキャンを追加することで、脆弱性を早期に検出できます。

# CI ワークフローに追加
- name: Security scan
  working-directory: ./backend
  run: |
    composer require --dev enlightn/enlightn
    php artisan enlightn

3. データベースバックアップ

デプロイ前にデータベースのバックアップを取得することをお勧めします:

# デプロイスクリプトに追加
TIMESTAMP=$(date +"%Y%m%d%H%M%S")
mysqldump -u $DB_USERNAME -p$DB_PASSWORD $DB_DATABASE > /backup/myapp_$TIMESTAMP.sql

4. ゼロダウンタイムデプロイ

ゼロダウンタイムデプロイを実現するために、Laravel Envoyを使用することができます:

Envoy.blade.php:

@servers(['web' => 'user@your-server.com'])

@setup
    $repository = 'git@github.com:username/repo.git';
    $app_dir = '/var/www/myapp';
    $release_dir = $app_dir . '/releases';
    $current = $app_dir . '/current';
    $release = 'release_' . date('YmdHis');
    $new_release_dir = $release_dir . '/' . $release;
@endsetup

@story('deploy')
    clone_repository
    run_composer
    update_symlinks
    update_permissions
    run_migrations
    build_frontend
    copy_frontend
    cache_config
    restart_services
    clean_old_releases
@endstory

@task('clone_repository')
    echo 'Cloning repository...'
    mkdir -p {{ $release_dir }}
    git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
    cd {{ $new_release_dir }}
    git checkout main
@endtask

@task('run_composer')
    echo "Running composer..."
    cd {{ $new_release_dir }}/backend
    composer install --no-dev --prefer-dist --optimize-autoloader
@endtask

@task('update_symlinks')
    echo "Updating symlinks..."
    ln -nfs {{ $new_release_dir }} {{ $current }}
    ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/backend/.env
    ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/backend/storage
@endtask

@task('update_permissions')
    echo "Updating permissions..."
    cd {{ $new_release_dir }}/backend
    chmod -R 775 storage bootstrap/cache
    chown -R www-data:www-data {{ $new_release_dir }}
@endtask

@task('run_migrations')
    echo "Running migrations..."
    cd {{ $new_release_dir }}/backend
    php artisan migrate --force
@endtask

@task('build_frontend')
    echo "Building frontend..."
    cd {{ $new_release_dir }}/frontend
    npm ci
    npm run build
@endtask

@task('copy_frontend')
    echo "Copying frontend build..."
    cp -r {{ $new_release_dir }}/frontend/build/* {{ $new_release_dir }}/backend/public/
@endtask

@task('cache_config')
    echo "Caching configuration..."
    cd {{ $new_release_dir }}/backend
    php artisan config:cache
    php artisan route:cache
    php artisan view:cache
@endtask

@task('restart_services')
    echo "Restarting services..."
    sudo systemctl reload nginx
    php artisan queue:restart
@endtask

@task('clean_old_releases')
    echo "Cleaning old releases..."
    cd {{ $release_dir }}
    ls -dt release_* | tail -n +6 | xargs -d "\n" rm -rf
@endtask

まとめ

この記事では、LaravelとReactを組み合わせたアプリケーションのCI/CDパイプラインと本番環境へのデプロイ方法について解説しました。主なポイントは以下の通りです:

  1. GitHub Actions(または他のCI/CDツール)を使用して、効率的なテストとデプロイパイプラインを構築する
  2. 自動テストを実装して、コードの品質とセキュリティを確保する
  3. VPS、Docker、クラウドサービス(AWS、Heroku)など、様々なデプロイオプションを検討する
  4. デプロイ後の自動化タスク(マイグレーション、キャッシュクリアなど)を設定する
  5. セキュリティ対策を講じる(環境変数の安全な管理、セキュリティスキャン、データベースバックアップなど)

これらの実践を取り入れることで、品質を維持しながら効率的なデプロイプロセスを実現できます。

参考リソース

  • Laravel 公式ドキュメント: https://laravel.com/docs/deployment
  • GitHub Actions 公式ドキュメント: https://docs.github.com/en/actions
  • Docker 公式ドキュメント: https://docs.docker.com/
  • AWS Elastic Beanstalk ドキュメント: https://docs.aws.amazon.com/elasticbeanstalk/
  • Heroku ドキュメント: https://devcenter.heroku.com/

コメント

タイトルとURLをコピーしました