NestJS デプロイ戦略:Blue-Green/Canary と DB マイグレーションの連携
本番環境へのデプロイは、開発者にとって最も緊張する瞬間の一つではないでしょうか。サービスのダウンタイムを最小限に抑えながら、新機能を安全にリリースする必要があります。
この記事では、NestJS アプリケーションにおける Blue-Green デプロイメントと Canary デプロイメントの実装方法、そして最も難易度の高いデータベースマイグレーションとの連携について詳しく解説します。実際のコード例とともに、本番運用で直面する課題とその解決策をご紹介しましょう。
背景
モダンなデプロイ戦略の必要性
従来の「サービスを停止してデプロイし、再起動する」方式では、ユーザーに不便を強いることになります。現代の Web サービスでは、24 時間 365 日の可用性が求められており、ゼロダウンタイムでのデプロイが必須要件となっています。
Blue-Green デプロイメントとは
Blue-Green デプロイメントは、本番環境(Blue)と同じ構成の待機環境(Green)を用意し、新バージョンを Green にデプロイ後、トラフィックを切り替える手法です。問題が発生した場合は即座に Blue へロールバックできるため、安全性が高いのが特徴ですね。
Canary デプロイメントとは
Canary デプロイメントは、新バージョンを一部のユーザーにのみ公開し、問題がないことを確認しながら段階的に全体へ展開する手法です。リスクを最小限に抑えながら、本番環境での動作を検証できます。
以下の図は、各デプロイ戦略の基本的なフローを示しています。
mermaidflowchart TD
Start["デプロイ開始"] --> Strategy{"デプロイ戦略"}
Strategy -->|Blue-Green| BG1["Green環境に<br/>新バージョンデプロイ"]
Strategy -->|Canary| C1["一部ユーザーに<br/>新バージョン公開"]
BG1 --> BG2["ヘルスチェック"]
BG2 -->|成功| BG3["トラフィック切替<br/>Blue → Green"]
BG2 -->|失敗| BG4["ロールバック"]
C1 --> C2["メトリクス監視"]
C2 -->|問題なし| C3["段階的に<br/>トラフィック増加"]
C2 -->|問題あり| C4["ロールバック"]
BG3 --> Done["デプロイ完了"]
C3 --> Done
BG4 --> End["デプロイ中止"]
C4 --> End
上の図から分かるように、どちらの戦略も安全性を重視し、問題発生時のロールバック機能を備えています。
課題
データベースマイグレーションの難しさ
デプロイ戦略を実装する上で最大の課題は、データベースマイグレーションとの連携です。アプリケーションコードは簡単にロールバックできますが、データベーススキーマの変更は後戻りが困難な場合があります。
Blue-Green デプロイメントにおける課題
| # | 課題 | 詳細 |
|---|---|---|
| 1 | スキーマの互換性 | 旧バージョン(Blue)と新バージョン(Green)が同時に稼働する期間、両方が同じ DB にアクセス可能である必要がある |
| 2 | リソースコスト | 本番環境と同じ規模の待機環境を常時維持するため、インフラコストが倍増する |
| 3 | データ同期 | 切り替え中のトランザクションデータを確実に保持する必要がある |
Canary デプロイメントにおける課題
Canary デプロイメントでは、旧バージョンと新バージョンが長期間並行稼働します。この期間中、データベーススキーマは両バージョンに対応していなければなりません。
以下の図は、DB マイグレーションとデプロイの関係を示しています。
mermaidsequenceDiagram
participant V1 as アプリv1.0
participant DB as データベース
participant V2 as アプリv2.0
Note over V1,DB: 通常運用
V1->>DB: SELECT * FROM users
DB-->>V1: 既存カラムで応答
Note over DB,V2: マイグレーション実行
V2->>DB: ALTER TABLE users<br/>ADD COLUMN new_field
Note over V1,V2: 並行稼働期間
V1->>DB: SELECT * FROM users
DB-->>V1: 新カラムを無視して応答
V2->>DB: SELECT * FROM users
DB-->>V2: 新カラムを含めて応答
Note over V1,V2: 問題発生!
V2->>DB: INSERT ... (new_field)
V1->>DB: SELECT ...
V1->>V1: new_fieldが<br/>想定外でエラー
このシーケンス図が示すように、並行稼働期間中のスキーマ変更は慎重に設計する必要があります。
よくあるエラーと原因
デプロイ時に発生しやすいエラーを以下にまとめました。
| # | エラーコード | 発生条件 | 原因 |
|---|---|---|---|
| 1 | ER_BAD_FIELD_ERROR: Unknown column | v1.0 が v2.0 のカラムを参照 | 後方互換性のないマイグレーション |
| 2 | ER_NO_DEFAULT_FOR_FIELD | v1.0 が新しい NOT NULL カラムに値を入れない | デフォルト値の未設定 |
| 3 | TypeORM: Cannot read property of undefined | エンティティ定義と DB スキーマの不一致 | マイグレーションタイミングの問題 |
解決策
後方互換性を保つマイグレーション戦略
データベースマイグレーションとデプロイを安全に連携させるには、「拡張フェーズ」と「収縮フェーズ」の 2 段階アプローチが効果的です。
拡張フェーズ(Expand Phase)
新しいスキーマを追加しますが、既存のスキーマは削除しません。この段階では、旧バージョンと新バージョンの両方が動作可能です。
収縮フェーズ(Contract Phase)
旧バージョンが完全に停止した後、不要になった古いスキーマを削除します。
以下の図は、このアプローチの全体像を表しています。
mermaidflowchart LR
subgraph Phase1["拡張フェーズ"]
E1["新カラム追加<br/>(NULL許可)"]
E2["新テーブル追加"]
E3["データ移行開始"]
end
subgraph Phase2["並行稼働"]
P1["v1.0: 旧カラム使用"]
P2["v2.0: 新カラム使用"]
P3["両バージョン稼働"]
end
subgraph Phase3["収縮フェーズ"]
C1["v1.0完全停止"]
C2["旧カラム削除"]
C3["制約追加"]
end
Phase1 --> Phase2
Phase2 --> Phase3
NestJS での実装アプローチ
NestJS では、TypeORM または Prisma を使用してマイグレーションを管理できます。ここでは、TypeORM を使用した実装例をご紹介します。
ステップ 1:マイグレーション設定の準備
まず、TypeORM の設定ファイルを作成しましょう。
typescript// ormconfig.ts
import { DataSource } from 'typeorm';
import { config } from 'dotenv';
config();
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
entities: ['dist/**/*.entity.js'],
migrations: ['dist/migrations/*.js'],
synchronize: false, // 本番環境では必ずfalse
});
この設定では、synchronize: false を指定することで、自動スキーマ同期を無効化しています。本番環境では、マイグレーションスクリプトによる明示的な管理が必須です。
ステップ 2:後方互換性のあるマイグレーション作成
拡張フェーズのマイグレーションを作成します。新しいカラムは必ず NULL 許可またはデフォルト値を設定しましょう。
typescript// migrations/1234567890-AddEmailVerified.ts
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddEmailVerified1234567890 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// 拡張フェーズ:新カラムを追加(NULL許可)
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'email_verified',
type: 'boolean',
isNullable: true, // 重要:既存レコードに影響を与えない
default: false,
}),
);
}
このマイグレーションでは、isNullable: true と default: false を設定することで、既存の v1.0 アプリケーションが影響を受けないようにしています。
続いて、ダウンマイグレーションも実装します。
typescript public async down(queryRunner: QueryRunner): Promise<void> {
// ロールバック時は追加したカラムを削除
await queryRunner.dropColumn('users', 'email_verified');
}
}
ステップ 3:エンティティの段階的更新
新バージョン(v2.0)のエンティティでは、新しいカラムを利用します。ただし、旧バージョンとの互換性を考慮した実装が必要です。
typescript// entities/user.entity.ts (v2.0)
import {
Entity,
Column,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
// 新しいカラム:NULLの可能性を考慮
@Column({ nullable: true, default: false })
emailVerified?: boolean;
}
emailVerified をオプショナル(?)にすることで、古いデータにも対応できますね。
ステップ 4:デプロイスクリプトの作成
Blue-Green デプロイメントを実現するためのスクリプトを作成します。まず、デプロイ前のヘルスチェック機能を実装しましょう。
typescript// scripts/health-check.ts
import axios from 'axios';
async function healthCheck(url: string, retries = 30): Promise<boolean> {
for (let i = 0; i < retries; i++) {
try {
const response = await axios.get(`${url}/health`);
if (response.status === 200 && response.data.status === 'ok') {
console.log('✓ ヘルスチェック成功');
return true;
}
} catch (error) {
console.log(`ヘルスチェック待機中... (${i + 1}/${retries})`);
}
このスクリプトは、新しくデプロイされた環境が正常に起動するまで待機します。
typescript // 5秒待機してリトライ
await new Promise(resolve => setTimeout(resolve, 5000));
}
console.error('✗ ヘルスチェック失敗');
return false;
}
export default healthCheck;
ステップ 5:Blue-Green デプロイメントスクリプト
Docker Compose を使用した Blue-Green デプロイメントの実装例です。
bash#!/bin/bash
# scripts/blue-green-deploy.sh
set -e
CURRENT_ENV=$(cat .current-env)
NEW_ENV="green"
if [ "$CURRENT_ENV" == "green" ]; then
NEW_ENV="blue"
fi
echo "現在の環境: $CURRENT_ENV"
echo "デプロイ先: $NEW_ENV"
まず、現在アクティブな環境を確認し、切り替え先の環境を決定します。
次に、マイグレーションを実行してから新環境を起動します。
bash# マイグレーション実行(拡張フェーズ)
echo "マイグレーション実行中..."
docker-compose run --rm migration yarn migration:run
# 新環境のコンテナを起動
echo "$NEW_ENV 環境を起動中..."
docker-compose up -d app-$NEW_ENV
# ヘルスチェック
echo "ヘルスチェック中..."
node dist/scripts/health-check.js http://localhost:300${NEW_ENV:0:1}
ヘルスチェックが成功したら、ロードバランサーの設定を切り替えます。
bashif [ $? -eq 0 ]; then
# トラフィックを新環境に切り替え
echo "トラフィックを $NEW_ENV に切り替え中..."
./scripts/switch-traffic.sh $NEW_ENV
# 旧環境を停止
sleep 30 # 既存接続の完了を待つ
docker-compose stop app-$CURRENT_ENV
# 現在の環境を更新
echo $NEW_ENV > .current-env
echo "✓ デプロイ完了"
else
echo "✗ デプロイ失敗"
exit 1
fi
ステップ 6:Canary デプロイメントの実装
Canary デプロイメントでは、Nginx やトラフィック管理ツールを使用して、段階的にトラフィックを新バージョンにルーティングします。
typescript// config/nginx-canary.conf
upstream backend {
# 90%のトラフィックを安定版に
server app-stable:3000 weight=90;
# 10%のトラフィックをCanaryに
server app-canary:3000 weight=10;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
}
}
この設定では、weight パラメータを使用してトラフィックの割合を制御しています。
トラフィックの割合を動的に変更するスクリプトも作成しましょう。
bash#!/bin/bash
# scripts/canary-rollout.sh
CANARY_PERCENTAGE=$1
if [ -z "$CANARY_PERCENTAGE" ]; then
echo "使用方法: ./canary-rollout.sh [0-100]"
exit 1
fi
STABLE_WEIGHT=$((100 - CANARY_PERCENTAGE))
パーセンテージからウェイト値を計算します。
bash# Nginx設定を動的に生成
cat > /etc/nginx/conf.d/canary.conf <<EOF
upstream backend {
server app-stable:3000 weight=${STABLE_WEIGHT};
server app-canary:3000 weight=${CANARY_PERCENTAGE};
}
EOF
# Nginxをリロード
nginx -s reload
echo "Canaryトラフィック: ${CANARY_PERCENTAGE}%"
ステップ 7:モニタリングとロールバック
デプロイ後のモニタリングは欠かせません。NestJS でメトリクス収集の仕組みを実装します。
typescript// monitoring/metrics.service.ts
import { Injectable } from '@nestjs/common';
import { Counter, Histogram } from 'prom-client';
@Injectable()
export class MetricsService {
private errorCounter: Counter;
private responseTime: Histogram;
constructor() {
this.errorCounter = new Counter({
name: 'http_errors_total',
help: 'HTTPエラーの総数',
labelNames: ['method', 'path', 'status'],
});
エラーカウンターとレスポンスタイムのヒストグラムを初期化します。
typescript this.responseTime = new Histogram({
name: 'http_response_time_seconds',
help: 'HTTPレスポンス時間',
labelNames: ['method', 'path'],
buckets: [0.1, 0.5, 1, 2, 5],
});
}
recordError(method: string, path: string, status: number): void {
this.errorCounter.inc({ method, path, status });
}
}
これらのメトリクスを使用して、Canary デプロイメントの健全性を監視できます。
自動ロールバックの判定ロジックも実装しましょう。
typescript// monitoring/rollback.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { MetricsService } from './metrics.service';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
@Injectable()
export class RollbackService {
private readonly logger = new Logger(RollbackService.name);
private readonly ERROR_THRESHOLD = 0.05; // 5%のエラー率
constructor(private metricsService: MetricsService) {}
エラー率の閾値を設定し、これを超えた場合に自動ロールバックを実行します。
typescript async checkAndRollback(): Promise<void> {
const metrics = await this.metricsService.getMetrics();
const errorRate = metrics.errors / metrics.total;
if (errorRate > this.ERROR_THRESHOLD) {
this.logger.error(
`エラー率が閾値を超過: ${errorRate * 100}%`,
);
await this.executeRollback();
}
}
エラー率が閾値を超えた場合の処理を実装します。
typescript private async executeRollback(): Promise<void> {
this.logger.warn('自動ロールバックを開始します');
try {
await execAsync('./scripts/rollback.sh');
this.logger.log('ロールバック完了');
} catch (error) {
this.logger.error('ロールバック失敗', error);
}
}
}
具体例
実践的なユースケース:ユーザーテーブルへのカラム追加
実際のプロジェクトで、ユーザーテーブルに新しいカラムを追加しながら Blue-Green デプロイメントを実施する例を見ていきましょう。
シナリオ
- 既存の v1.0 アプリケーションが稼働中
- ユーザーテーブルに
phone_numberカラムを追加したい - ゼロダウンタイムでデプロイしたい
以下の図は、この実装の全体フローを示しています。
mermaidflowchart TD
Start["v1.0稼働中"] --> M1["マイグレーション1:<br/>phone_number追加<br/>(NULL許可)"]
M1 --> Deploy["v2.0をGreenに<br/>デプロイ"]
Deploy --> Health["ヘルスチェック"]
Health -->|成功| Switch["トラフィック切替"]
Health -->|失敗| Rollback1["ロールバック"]
Switch --> Monitor["24時間監視"]
Monitor -->|問題なし| M2["マイグレーション2:<br/>v1.0停止後に<br/>NOT NULL制約追加"]
Monitor -->|問題あり| Rollback2["v1.0に戻す"]
M2 --> Complete["デプロイ完了"]
Rollback1 --> End["デプロイ中止"]
Rollback2 --> End
ステップ 1:拡張フェーズのマイグレーション作成
まず、NULL 許可の新カラムを追加するマイグレーションを作成します。
typescript// migrations/1234567891-AddPhoneNumber.ts
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddPhoneNumber1234567891 implements MigrationInterface {
name = 'AddPhoneNumber1234567891';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'phone_number',
type: 'varchar',
length: '20',
isNullable: true, // v1.0との互換性のため
}),
);
}
このように、isNullable: true を指定することで、既存の v1.0 アプリケーションが影響を受けません。
typescript public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'phone_number');
}
}
ステップ 2:v2.0 のエンティティ更新
新しいカラムを使用するエンティティを定義します。
typescript// entities/user.entity.ts (v2.0)
import {
Entity,
Column,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column({ nullable: true })
phoneNumber?: string; // オプショナルにする
}
phoneNumber をオプショナルにすることで、マイグレーション直後のデータ(phone_number が NULL)にも対応できます。
ステップ 3:アプリケーションロジックの実装
新しいカラムを使用するサービスを実装しましょう。
typescript// users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
ユーザーサービスのコンストラクタでリポジトリを注入します。
typescript async updatePhoneNumber(
userId: number,
phoneNumber: string,
): Promise<User> {
const user = await this.usersRepository.findOne({
where: { id: userId },
});
if (!user) {
throw new Error('User not found');
}
user.phoneNumber = phoneNumber;
return this.usersRepository.save(user);
}
}
この実装では、既存のユーザーに対して電話番号を追加できますね。
ステップ 4:完全なデプロイスクリプト
実際に使用するデプロイスクリプトの完全版を見ていきましょう。
bash#!/bin/bash
# deploy.sh - 完全版Blue-Greenデプロイメントスクリプト
set -euo pipefail
# 設定
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
CURRENT_ENV=$(cat "$PROJECT_ROOT/.current-env" || echo "blue")
NEW_ENV="green"
if [ "$CURRENT_ENV" == "green" ]; then
NEW_ENV="blue"
fi
環境変数と現在のアクティブ環境を設定します。
bashecho "========================================="
echo "Blue-Greenデプロイメント開始"
echo "========================================="
echo "現在の環境: $CURRENT_ENV"
echo "デプロイ先: $NEW_ENV"
echo "========================================="
# ステップ1: マイグレーション実行
echo "[1/6] データベースマイグレーション実行中..."
docker-compose run --rm migration yarn migration:run
if [ $? -ne 0 ]; then
echo "✗ マイグレーション失敗"
exit 1
fi
echo "✓ マイグレーション完了"
まず、データベースマイグレーションを実行します。これは拡張フェーズのマイグレーションです。
bash# ステップ2: 新環境のビルド
echo "[2/6] $NEW_ENV 環境のDockerイメージビルド中..."
docker-compose build app-$NEW_ENV
echo "✓ ビルド完了"
# ステップ3: 新環境の起動
echo "[3/6] $NEW_ENV 環境を起動中..."
docker-compose up -d app-$NEW_ENV
sleep 10 # 起動待ち
新しい環境の Docker イメージをビルドし、コンテナを起動します。
bash# ステップ4: ヘルスチェック
echo "[4/6] ヘルスチェック実行中..."
HEALTH_URL="http://localhost:300$([ "$NEW_ENV" == "blue" ] && echo "1" || echo "2")"
for i in {1..30}; do
if curl -f "$HEALTH_URL/health" > /dev/null 2>&1; then
echo "✓ ヘルスチェック成功"
break
fi
if [ $i -eq 30 ]; then
echo "✗ ヘルスチェックタイムアウト"
docker-compose logs app-$NEW_ENV
docker-compose stop app-$NEW_ENV
exit 1
fi
echo " 待機中... ($i/30)"
sleep 5
done
新環境が正常に起動したことを確認するため、ヘルスチェックを実施します。
bash# ステップ5: トラフィック切り替え
echo "[5/6] トラフィックを $NEW_ENV に切り替え中..."
./scripts/switch-traffic.sh $NEW_ENV
if [ $? -ne 0 ]; then
echo "✗ トラフィック切り替え失敗"
exit 1
fi
echo "✓ トラフィック切り替え完了"
sleep 30 # 既存接続の完了を待つ
ロードバランサーの設定を変更して、トラフィックを新環境に切り替えます。
bash# ステップ6: 旧環境の停止
echo "[6/6] $CURRENT_ENV 環境を停止中..."
docker-compose stop app-$CURRENT_ENV
# 現在の環境を更新
echo $NEW_ENV > "$PROJECT_ROOT/.current-env"
echo "========================================="
echo "✓ デプロイ完了"
echo "アクティブ環境: $NEW_ENV"
echo "========================================="
ステップ 5:ロールバックスクリプト
問題が発生した場合に備えて、ロールバックスクリプトも用意します。
bash#!/bin/bash
# rollback.sh - ロールバックスクリプト
set -euo pipefail
CURRENT_ENV=$(cat .current-env)
PREVIOUS_ENV="blue"
if [ "$CURRENT_ENV" == "blue" ]; then
PREVIOUS_ENV="green"
fi
echo "========================================="
echo "ロールバック開始"
echo "========================================="
echo "現在の環境: $CURRENT_ENV → $PREVIOUS_ENV"
現在の環境と戻す先の環境を特定します。
bash# 前の環境を再起動
echo "[1/3] $PREVIOUS_ENV 環境を起動中..."
docker-compose up -d app-$PREVIOUS_ENV
sleep 10
# ヘルスチェック
echo "[2/3] ヘルスチェック中..."
HEALTH_URL="http://localhost:300$([ "$PREVIOUS_ENV" == "blue" ] && echo "1" || echo "2")"
if ! curl -f "$HEALTH_URL/health" > /dev/null 2>&1; then
echo "✗ ロールバック失敗"
exit 1
fi
前の環境を起動し、正常に動作することを確認します。
bash# トラフィックを戻す
echo "[3/3] トラフィックを $PREVIOUS_ENV に戻し中..."
./scripts/switch-traffic.sh $PREVIOUS_ENV
# 現在の環境を停止
docker-compose stop app-$CURRENT_ENV
# 環境ファイルを更新
echo $PREVIOUS_ENV > .current-env
echo "========================================="
echo "✓ ロールバック完了"
echo "========================================="
ステップ 6:収縮フェーズのマイグレーション
v1.0 が完全に停止し、v2.0 のみが稼働している状態になったら、収縮フェーズのマイグレーションを実行できます。
typescript// migrations/1234567892-MakePhoneNumberRequired.ts
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MakePhoneNumberRequired1234567892 implements MigrationInterface {
name = 'MakePhoneNumberRequired1234567892';
public async up(queryRunner: QueryRunner): Promise<void> {
// NULLのレコードにデフォルト値を設定
await queryRunner.query(
`UPDATE users SET phone_number = '' WHERE phone_number IS NULL`
);
まず、NULL のレコードにデフォルト値を設定します。
typescript // NOT NULL制約を追加
await queryRunner.query(
`ALTER TABLE users ALTER COLUMN phone_number SET NOT NULL`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE users ALTER COLUMN phone_number DROP NOT NULL`
);
}
}
これで、phone_number カラムが必須フィールドになりました。
エラー処理とトラブルシューティング
ケース 1:マイグレーション失敗時のエラー
エラーコード: QueryFailedError: ER_DUP_FIELDNAME: Duplicate column name 'phone_number'
発生条件: 既に同じカラムが存在する状態でマイグレーションを実行
解決方法:
- 既存のマイグレーション状態を確認
bashyarn migration:show
- 重複したマイグレーションをリバートする
bashyarn migration:revert
- マイグレーションを再実行する
bashyarn migration:run
ケース 2:ヘルスチェック失敗
エラーコード: ECONNREFUSED: Connection refused
発生条件: アプリケーションの起動が完了していない、またはポートが間違っている
解決方法:
- コンテナのログを確認
bashdocker-compose logs app-green
- アプリケーションの起動エラーがないか確認
typescript// main.ts - エラーハンドリングを追加
async function bootstrap() {
try {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log('アプリケーション起動完了');
} catch (error) {
console.error('起動エラー:', error);
process.exit(1);
}
}
- データベース接続を確認
bashdocker-compose exec postgres psql -U myuser -d mydb -c "\dt"
ケース 3:トラフィック切り替え後のエラー増加
エラーコード: TypeError: Cannot read property 'phoneNumber' of undefined
発生条件: v2.0 のコードが NULL を適切に処理していない
解決方法:
- 即座にロールバックを実行
bash./scripts/rollback.sh
- コードに NULL チェックを追加
typescript// users/users.controller.ts
@Get(':id/phone')
async getPhoneNumber(@Param('id') id: number) {
const user = await this.usersService.findOne(id);
// NULLチェックを追加
if (!user) {
throw new NotFoundException('ユーザーが見つかりません');
}
return {
phoneNumber: user.phoneNumber ?? '未設定',
};
}
- 修正後、再度デプロイを実行
Docker Compose 設定の完全版
最後に、Blue-Green デプロイメントを実現するための Docker Compose 設定をご紹介します。
yaml# docker-compose.yml
version: '3.8'
services:
# データベース(共有)
postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- '5432:5432'
データベースは両環境で共有します。
yaml# Blue環境
app-blue:
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: production
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: myuser
DB_PASSWORD: mypassword
DB_DATABASE: mydb
ports:
- '3001:3000'
depends_on:
- postgres
Blue 環境はポート 3001 で公開します。
yaml# Green環境
app-green:
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: production
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: myuser
DB_PASSWORD: mypassword
DB_DATABASE: mydb
ports:
- '3002:3000'
depends_on:
- postgres
Green 環境はポート 3002 で公開します。
yaml # マイグレーション実行用
migration:
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: production
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: myuser
DB_PASSWORD: mypassword
DB_DATABASE: mydb
depends_on:
- postgres
command: yarn migration:run
volumes:
postgres_data:
まとめ
この記事では、NestJS アプリケーションにおける Blue-Green デプロイメントと Canary デプロイメントの実装方法、そしてデータベースマイグレーションとの連携について詳しく解説しました。
重要なポイント
| # | 項目 | ポイント |
|---|---|---|
| 1 | マイグレーション戦略 | 拡張フェーズと収縮フェーズの 2 段階アプローチで後方互換性を確保する |
| 2 | Blue-Green デプロイ | ヘルスチェックとロールバック機能を必ず実装する |
| 3 | Canary デプロイ | トラフィックを段階的に増やし、メトリクスを監視する |
| 4 | エラー処理 | NULL チェックと適切なデフォルト値の設定が重要 |
| 5 | モニタリング | エラー率を監視し、閾値を超えたら自動ロールバック |
実装の流れ
- 拡張フェーズ: 新しいスキーマを NULL 許可で追加し、両バージョンが動作可能な状態を作る
- デプロイ: ヘルスチェックを実施してから、トラフィックを切り替える
- モニタリング: エラー率やレスポンスタイムを監視し、問題があればロールバック
- 収縮フェーズ: 旧バージョンが完全に停止したら、不要なスキーマを削除
今後の発展
本記事で紹介した基本的な実装をベースに、以下のような拡張も検討できます。
- Kubernetes を使用した自動スケーリング対応
- A/B テストとの統合
- 複数リージョンへの段階的デプロイ
- Feature フラグによるきめ細かいリリースコントロール
安全で確実なデプロイ戦略を実装することで、開発チームは自信を持って新機能をリリースできるようになります。本番環境へのデプロイも、もう怖くありませんね。
ぜひ、この記事を参考に、あなたのプロジェクトでもゼロダウンタイムデプロイメントを実現してください。
関連リンク
articleNestJS デプロイ戦略:Blue-Green/Canary と DB マイグレーションの連携
articleNestJS 認可設計:RBAC/ABAC/ポリシーベース(CASL/oso)の実装指針
articleNestJS Guard/Interceptor/Filter 早見表:適用順序とユースケース対応表
articleNestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理
articleNestJS × TypeORM vs Prisma vs Drizzle:DX・性能・移行性の総合ベンチ
articleNestJS メモリリーク診断:Node.js Profiler と Heap Snapshot で原因を掴む
articleObsidian タスク運用の最適解:Tasks + Periodic Notes で計画と実行を接続
articlePreact Signals チートシート:signal/computed/effect 実用スニペット 30
articleNuxt パフォーマンス運用:payload/コード分割/プリフェッチの継続的チューニング
articleNginx キャパシティプランニング:worker_processes/connections/reuseport の算定メソッド
articlePlaywright スクリーンショット/動画取得のベストプラクティス集【設定例付き】
articleNestJS デプロイ戦略:Blue-Green/Canary と DB マイグレーションの連携
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来