はじめに
モダンなウェブアプリケーション開発において、継続的インテグレーション(CI)と継続的デリバリー(CD)のパイプラインを構築することは、品質を維持しながら迅速にデプロイを行うために不可欠です。本記事では、Laravel(バックエンド)とReact(フロントエンド)を組み合わせたアプリケーションの CI/CD パイプラインの構築方法と、本番環境へのデプロイ方法について解説します。
具体的には、以下のトピックをカバーします:
- CI/CD パイプラインの基本概念
- GitHub Actions を使った CI/CD パイプラインの構築
- 自動テストの設定
- 本番環境へのデプロイ方法(複数の選択肢)
- デプロイ後の自動化タスク
- セキュリティ対策
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パイプラインと本番環境へのデプロイ方法について解説しました。主なポイントは以下の通りです:
- GitHub Actions(または他のCI/CDツール)を使用して、効率的なテストとデプロイパイプラインを構築する
- 自動テストを実装して、コードの品質とセキュリティを確保する
- VPS、Docker、クラウドサービス(AWS、Heroku)など、様々なデプロイオプションを検討する
- デプロイ後の自動化タスク(マイグレーション、キャッシュクリアなど)を設定する
- セキュリティ対策を講じる(環境変数の安全な管理、セキュリティスキャン、データベースバックアップなど)
これらの実践を取り入れることで、品質を維持しながら効率的なデプロイプロセスを実現できます。
参考リソース
- 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/
コメント