Chuyển đến nội dung chính

レッスン 20: 実稼働環境の導入と監視

実稼働戦略の展開、Nginx リバース プロキシ、PM2 クラスター。ヘルスチェック、ロギング、Prometheus メトリクス、Grafana ダッシュボード。

💻 プログラミング — レッスン 20 レッスン 20: 実稼働環境のデプロイメントと モニタリング

NestJS: 基本から高度まで

パート 5: マイクロサービス、テスト、本番環境

xdev.asia

1. 健康診断

npm install @nestjs/terminus
// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import {
  HealthCheck,
  HealthCheckService,
  TypeOrmHealthIndicator,
  MemoryHealthIndicator,
  DiskHealthIndicator,
  HttpHealthIndicator,
} from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private memory: MemoryHealthIndicator,
    private disk: DiskHealthIndicator,
    private http: HttpHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      // Database
      () => this.db.pingCheck('database'),
      // Memory: heap < 300MB
      () => this.memory.checkHeap('memory_heap', 300 * 1024 * 1024),
      // Disk: < 90% used
      () => this.disk.checkStorage('disk', {
        thresholdPercent: 0.9,
        path: '/',
      }),
    ]);
  }

  @Get('ready')
  @HealthCheck()
  readiness() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }

  @Get('live')
  @HealthCheck()
  liveness() {
    return this.health.check([
      () => this.memory.checkHeap('memory', 500 * 1024 * 1024),
    ]);
  }
}

2. 構造化されたロギング

npm install winston nest-winston
// main.ts
import { WinstonModule, utilities } from 'nest-winston';
import * as winston from 'winston';

const app = await NestFactory.create(AppModule, {
  logger: WinstonModule.createLogger({
    transports: [
      // Console (development)
      new winston.transports.Console({
        format: winston.format.combine(
          winston.format.timestamp(),
          utilities.format.nestLike('NestApp'),
        ),
      }),
      // File (production)
      new winston.transports.File({
        filename: 'logs/error.log',
        level: 'error',
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.json(),
        ),
        maxsize: 10 * 1024 * 1024,  // 10MB
        maxFiles: 5,
      }),
      new winston.transports.File({
        filename: 'logs/combined.log',
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.json(),
        ),
      }),
    ],
  }),
});

3. プロメテウスのメトリクス

npm install prom-client @willsoto/nestjs-prometheus
// app.module.ts
import { PrometheusModule } from '@willsoto/nestjs-prometheus';

@Module({
  imports: [
    PrometheusModule.register({
      path: '/metrics',
      defaultMetrics: { enabled: true },
    }),
  ],
})
export class AppModule {}
// Custom metrics
import { Counter, Histogram } from 'prom-client';
import { InjectMetric, makeCounterProvider, makeHistogramProvider } from '@willsoto/nestjs-prometheus';

// Đăng ký trong module
@Module({
  providers: [
    makeCounterProvider({
      name: 'http_requests_total',
      help: 'Total HTTP requests',
      labelNames: ['method', 'path', 'status'],
    }),
    makeHistogramProvider({
      name: 'http_request_duration_seconds',
      help: 'HTTP request duration',
      labelNames: ['method', 'path'],
      buckets: [0.01, 0.05, 0.1, 0.5, 1, 5],
    }),
  ],
})
export class MetricsModule {}

// Interceptor thu thập metrics
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
  constructor(
    @InjectMetric('http_requests_total') private counter: Counter,
    @InjectMetric('http_request_duration_seconds') private histogram: Histogram,
  ) {}

  intercept(context: ExecutionContext, next: CallHandler) {
    const req = context.switchToHttp().getRequest();
    const { method, route } = req;
    const path = route?.path || req.url;
    const timer = this.histogram.startTimer({ method, path });

    return next.handle().pipe(
      tap(() => {
        const status = context.switchToHttp().getResponse().statusCode;
        this.counter.inc({ method, path, status });
        timer();
      }),
    );
  }
}

4. Nginx リバースプロキシ

# nginx/default.conf
upstream nestapp {
    server app:3000;
}

server {
    listen 80;
    server_name api.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    # Security headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

    # Gzip
    gzip on;
    gzip_types application/json text/plain;

    # Rate limiting
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    location / {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://nestapp;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }

    # Health check (không rate limit)
    location /health {
        proxy_pass http://nestapp;
        access_log off;
    }

    # Metrics (restrict access)
    location /metrics {
        allow 10.0.0.0/8;
        deny all;
        proxy_pass http://nestapp;
    }
}

5. PM2クラスターモード

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'nestapp',
    script: 'dist/main.js',
    instances: 'max',       // Số CPU cores
    exec_mode: 'cluster',
    max_memory_restart: '500M',
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
    // Graceful shutdown
    kill_timeout: 5000,
    listen_timeout: 10000,
    // Logging
    error_file: './logs/pm2-error.log',
    out_file: './logs/pm2-out.log',
    merge_logs: true,
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
  }],
};
// main.ts — Graceful shutdown
app.enableShutdownHooks();

// Trong service
@Injectable()
export class AppService implements OnModuleDestroy {
  async onModuleDestroy() {
    // Đóng connections
    await this.dataSource.destroy();
    await this.redisClient.quit();
    this.logger.log('Graceful shutdown completed');
  }
}

6. スタックの監視 (Docker Compose)

# docker-compose.monitoring.yml
services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3001:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-data:/var/lib/grafana

volumes:
  grafana-data:
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'nestapp'
    static_configs:
      - targets: ['app:3000']
    metrics_path: /metrics

7. 制作チェックリスト

カテゴリ詳細
セキュリティヘルメット、CORS、レート制限、入力検証
パフォーマンス圧縮、キャッシュ、接続プーリング
信頼性ヘルスチェック、正常なシャットダウン、再試行ロジック
可観測性構造化ロギング、メトリクス、トレース
CI/CD自動テスト、Docker ビルド、パイプラインのデプロイ
インフラストラクチャーリバースプロキシ、SSL、ロードバランシング

8. シリーズの概要

20 のレッスンを通じて、NestJS の基礎から運用までをマスターしました。

  • パート 1: TypeScript、コントローラー、ルーティング — コア基盤
  • パート 2: DI、モジュール、データベース、検証 — データ層の構築
  • パート 3: 認証、認可、セキュリティ — アプリケーションのセキュリティ
  • パート 4: WebSocket、GraphQL、キャッシング、スケジューリング — 高度な機能
  • パート 5: マイクロサービス、テスト、Docker、実稼働 — 実際の展開

NestJS での成功を祈っています。