📄
概念 📚 beginner-stepup

CloudWatch Logs の読み方と運用(Month 6 Week 23)

EC2 上の Docker コンテナログを CloudWatch Logs に転送し、Logs Insights でクエリし、エラー検知アラームを設定する

Week 23 後半では、本番で発生した異常を 気付ける状態 にします。App EC2 の Docker コンテナが吐くログを CloudWatch Logs に転送し、エラー多発時にメール通知を受ける仕組みを作ります。


なぜローカルログでは足りないか

本番の App EC2 は Private Subnet にあり、Bastion 経由でしか入れません。「最近の投稿が失敗している」という報告を受けたとき、毎回 Bastion → App EC2 → docker logs するのは現実的ではありません。

CloudWatch Logs に集約すれば:

  • ブラウザから全ログを検索可能
  • Logs Insights で集計クエリ(「過去 1 時間の 5xx エラー」等)
  • Metric Filter + Alarm で閾値超過時に自動通知
  • EC2 が死んでもログは残る

設計:ログの流れ

[Hono API (Docker)]
  ↓ console.log → stdout
[Docker daemon]
  ↓ awslogs log driver
[CloudWatch Logs]
  /ecs/sns-clone-api/production

  [Logs Insights]  ← クエリで絞り込み
  [Metric Filter]  ← エラー数を Metric 化

  [Alarm]          ← 閾値超過で通知

  [SNS Topic]      ← メール / Slack

Step 1: Log Group を作成

AWS Console → CloudWatch → Log groups → Create log group

項目
Log group name/ec2/sns-clone-api/production
Retention setting7 days(カリキュラム)/ 30〜90 days(本番)

Why Retention: デフォルトは「Never Expire」で課金が積もります。必ず設定。


Step 2: App EC2 の IAM ロールに権限追加

既存の App EC2 Instance Profile に:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogStreams"
      ],
      "Resource": "arn:aws:logs:ap-northeast-1:*:log-group:/ec2/sns-clone-api/*"
    }
  ]
}

Step 3: Docker の log driver を awslogs に設定

docker-compose.prod.yml(b19 で作成したファイル)を更新:

services:
  api:
    image: ${IMAGE_URI}
    restart: unless-stopped
    environment:
      DATABASE_URL: ${DATABASE_URL}
      JWT_SECRET: ${JWT_SECRET}
      NODE_ENV: production
    ports:
      - "3001:3001"
    logging:
      driver: awslogs
      options:
        awslogs-region: ap-northeast-1
        awslogs-group: /ec2/sns-clone-api/production
        awslogs-stream: ${HOSTNAME}-api
        awslogs-create-group: "false"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
      interval: 30s
      timeout: 5s
      retries: 3

再デプロイすると、以降のログが CloudWatch に流れ始めます。

検証

# Bastion 経由で App EC2 に入り、適当にリクエストを発火
ssh app
curl http://localhost:3001/health

CloudWatch Console → Log groups → /ec2/sns-clone-api/production → Log streams → <hostname>-api{"status":"ok"} 周辺のログが見えれば OK。


Step 4: アプリ側のログを構造化

console.log のままだと検索性が悪いので JSON 構造化ログに昇格します。

api/src/lib/logger.ts(簡易版 pino ラッパ、依存追加なし):

type LogLevel = "debug" | "info" | "warn" | "error";

function log(level: LogLevel, message: string, meta?: Record<string, unknown>) {
  const entry = {
    level,
    timestamp: new Date().toISOString(),
    message,
    ...meta,
  };
  // stdout に 1 行 JSON → CloudWatch Logs が JSON として認識
  console.log(JSON.stringify(entry));
}

export const logger = {
  debug: (msg: string, meta?: Record<string, unknown>) => log("debug", msg, meta),
  info: (msg: string, meta?: Record<string, unknown>) => log("info", msg, meta),
  warn: (msg: string, meta?: Record<string, unknown>) => log("warn", msg, meta),
  error: (msg: string, meta?: Record<string, unknown>) => log("error", msg, meta),
};

api/src/index.tsapp.onError で使用(b14 の続き):

import { logger } from "./lib/logger";

app.onError((err, c) => {
  if (err instanceof AppError) {
    // 想定内の業務エラーは info レベル
    logger.info("handled app error", {
      path: c.req.path,
      method: c.req.method,
      code: err.code,
      status: err.status,
    });
    return c.json({ error: err.code, message: err.message, details: err.details }, err.status);
  }

  // 想定外のエラーは error レベル(Alarm のトリガ)
  logger.error("unhandled error", {
    path: c.req.path,
    method: c.req.method,
    name: err.name,
    stack: err.stack,
  });
  return c.json({ error: "INTERNAL_ERROR", message: "サーバー内部エラー" }, 500);
});

本格運用では pino を入れる:

cd api && pnpm add pino

Step 5: Logs Insights で検索

CloudWatch Console → Logs Insights → Log group を選択 → Run query:

よく使うクエリ

過去 1 時間のエラーログ

fields @timestamp, message, path, name, stack
| filter level = "error"
| sort @timestamp desc
| limit 100

5xx を返したパス別集計

fields @timestamp, path
| filter level = "error"
| stats count() by path
| sort count desc

レスポンス時間の p95: (レスポンス時間を logger.info で出している前提)

fields @timestamp, responseTime
| filter ispresent(responseTime)
| stats pct(responseTime, 95) by bin(5m)

特定ユーザーの操作履歴

fields @timestamp, message, path
| filter userId = "uuid-here"
| sort @timestamp asc

Step 6: Metric Filter + Alarm

Metric Filter

CloudWatch → Log groups → 対象 Log group → Metric filters → Create:

項目
Filter pattern{ $.level = "error" }
Metric namespaceSnsClone
Metric nameApiErrorCount
Metric value1
Default value0

Alarm

CloudWatch → Alarms → Create alarm → Metric SnsClone/ApiErrorCount

項目
StatisticSum
Period5 minutes
Threshold typeStatic
ConditionGreater than 10
Treat missing data asnot breaching

SNS Topic(通知先)

SNS → Topics → Create → Subscription にメールアドレス。登録後、メールのリンクを踏んで承認。

Alarm 作成画面で上記 Topic を選択。これで 5 分間にエラーが 10 件超えたらメールが飛びます。


運用のコツ

ダッシュボード化

CloudWatch Dashboards で以下を可視化:

  • API リクエスト数(時系列)
  • 5xx エラー数(時系列)
  • レスポンス時間 p50 / p95
  • ログイン成功/失敗数

PR レビューや Month 6 最終デモでスクリーンショットとして使えます。

ログ調査の型

  1. Alarm メールが届く
  2. CloudWatch Console → Alarms → 該当 Alarm → View in Logs Insights(ボタンあり)
  3. 自動的にその時間帯の Log group で Insights が開く
  4. エラーメッセージを読み、stack trace から該当コード箇所を特定
  5. PR を切って修正、テストを追加(Vitest + Playwright)

コスト管理

  • Log Group の Retention を設定(7 日 or 30 日)
  • Logs Insights のクエリ実行時間に比例した料金。重いクエリは bin(1m)bin(5m) で粒度を下げる
  • Metric Filter は無料、Custom Metric は最初の 10 個は無料枠

Week 23 のアウトプット

  • /ec2/sns-clone-api/production Log group が retention 7 日で作成されている
  • docker-compose.prod.yml に awslogs log driver 設定が入っている
  • ☐ App EC2 の Instance Profile に logs 書き込み権限がある
  • ☐ アプリが JSON 構造化ログを吐いている
  • ☐ Logs Insights で過去のエラーログが検索できる
  • ☐ Metric Filter + Alarm + SNS Email 通知が設定され、テストメールが届く

よくある失敗

  • Log Group に Retention を設定しない: 永遠に課金。必ず最初に設定
  • awslogs-create-group: “true”: デプロイ毎に Group を作ろうとして権限エラー。事前に Group を作り "false" にする
  • console.log だけで構造化しない: 検索効率が 10 倍違う。JSON 出力を習慣化
  • Alarm の閾値が厳しすぎる/緩すぎる: 最初は閾値を緩めにし、誤報/見逃しのバランスで調整
  • Alarm だけで満足: 通知が来ない = 安心ではない。週 1 でダッシュボードを目視確認

参考

生きているコード

本ドキュメントで扱ったパターンの完全な動作コードは、メンター側リポジトリの参照ブランチで確認できます。

  • 対応 Week: W23
  • 参照ブランチ:
  • W23: reference/week-23
  • 対応 checklist 項目: M12

ブランチの作り方・見方は b00-curriculum-map を参照してください。

📚 beginner-stepup 全 53 章
導入編 13 章
  1. 1. 📄Web とは何か
  2. 2. 📄URL を打ってから画面が表示されるまで
  3. 3. 📄ネットワーク基礎(TCP/IP・DNS・HTTPS)
  4. 4. 📄【発展】物理層から通信が成立するまで(電力・Ethernet・Wi-Fi・Bluetooth)
  5. 5. 📄WSL2・Docker セットアップ詳細(Windows 向け)
  6. 6. 📄環境構築の段階的導入(macOS / Windows)
  7. 7. 📄カリキュラム全体マップ(Week × 教材 × 参照ブランチ × 要求チェックリスト)
  8. 8. 📄このカリキュラムの使い方(SQL・Python・Dify経験者向け)
  9. 9. 📄シェル・ターミナル基礎
  10. 10. 📄Windows で完全にゼロから始める開発環境構築(Week 1)
  11. 11. 📄Git基礎
  12. 12. 📄GitHubワークフロー
  13. 13. 📄パッケージ管理(pnpm workspace)
応用編 16 章 ● この編を閲覧中
  1. 1. 📄AWSインフラ基礎
  2. 2. 📄AWS Budget Alert の設定(Month 5 Week 17)
  3. 3. 📄環境変数管理
  4. 4. 📄Bastion EC2 と SSH ProxyJump(Month 5 Week 18)
  5. 5. 📄CI/CD基礎
  6. 6. 📄ECR への Docker イメージ push と App EC2 デプロイ(Month 5 Week 19)
  7. 7. 📄テスト設計の基本
  8. 8. 📄CloudFront + S3 + ALB で公開する(Month 5 Week 20)
  9. 9. 📄CLAUDE.md・プロジェクト設定
  10. 10. 📄PR レビュー 5 観点ルーブリック(全 Week 共通)
  11. 11. 📄タスク分解・仕様の書き方
  12. 12. 📄Playwright で E2E テスト(Month 6 Week 22)
  13. 13. 📄生成コードのレビュー・デバッグの勘所
  14. 14. 📄Trivy で脆弱性スキャン(Month 6 Week 23)
  15. 15. 📄CloudWatch Logs の読み方と運用(Month 6 Week 23)
  16. 16. 📄PDF ポートフォリオの自動生成(Month 6 Week 24)