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 setting | 7 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.ts の app.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 namespace | SnsClone |
| Metric name | ApiErrorCount |
| Metric value | 1 |
| Default value | 0 |
Alarm
CloudWatch → Alarms → Create alarm → Metric SnsClone/ApiErrorCount:
| 項目 | 値 |
|---|---|
| Statistic | Sum |
| Period | 5 minutes |
| Threshold type | Static |
| Condition | Greater than 10 |
| Treat missing data as | not breaching |
SNS Topic(通知先)
SNS → Topics → Create → Subscription にメールアドレス。登録後、メールのリンクを踏んで承認。
Alarm 作成画面で上記 Topic を選択。これで 5 分間にエラーが 10 件超えたらメールが飛びます。
運用のコツ
ダッシュボード化
CloudWatch Dashboards で以下を可視化:
- API リクエスト数(時系列)
- 5xx エラー数(時系列)
- レスポンス時間 p50 / p95
- ログイン成功/失敗数
PR レビューや Month 6 最終デモでスクリーンショットとして使えます。
ログ調査の型
- Alarm メールが届く
- CloudWatch Console → Alarms → 該当 Alarm → View in Logs Insights(ボタンあり)
- 自動的にその時間帯の Log group で Insights が開く
- エラーメッセージを読み、stack trace から該当コード箇所を特定
- 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/productionLog 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 でダッシュボードを目視確認
参考
- CloudWatch Logs: https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/
- Logs Insights クエリ構文: https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/CWL_QuerySyntax.html
- Docker awslogs log driver: https://docs.docker.com/config/containers/logging/awslogs/
- pino: https://getpino.io/
- 前提: b19-ecr-push(App EC2 デプロイ)/ b14-error-handling(エラー分類)
- 続き: b24-portfolio-pdf(修了成果物)
生きているコード
本ドキュメントで扱ったパターンの完全な動作コードは、メンター側リポジトリの参照ブランチで確認できます。
- 対応 Week: W23
- 参照ブランチ:
- W23:
reference/week-23 - 対応 checklist 項目: M12
ブランチの作り方・見方は b00-curriculum-map を参照してください。
- 1. 📄Web とは何か
- 2. 📄URL を打ってから画面が表示されるまで
- 3. 📄ネットワーク基礎(TCP/IP・DNS・HTTPS)
- 4. 📄【発展】物理層から通信が成立するまで(電力・Ethernet・Wi-Fi・Bluetooth)
- 5. 📄WSL2・Docker セットアップ詳細(Windows 向け)
- 6. 📄環境構築の段階的導入(macOS / Windows)
- 7. 📄カリキュラム全体マップ(Week × 教材 × 参照ブランチ × 要求チェックリスト)
- 8. 📄このカリキュラムの使い方(SQL・Python・Dify経験者向け)
- 9. 📄シェル・ターミナル基礎
- 10. 📄Windows で完全にゼロから始める開発環境構築(Week 1)
- 11. 📄Git基礎
- 12. 📄GitHubワークフロー
- 13. 📄パッケージ管理(pnpm workspace)
- 1. 📄AWSインフラ基礎
- 2. 📄AWS Budget Alert の設定(Month 5 Week 17)
- 3. 📄環境変数管理
- 4. 📄Bastion EC2 と SSH ProxyJump(Month 5 Week 18)
- 5. 📄CI/CD基礎
- 6. 📄ECR への Docker イメージ push と App EC2 デプロイ(Month 5 Week 19)
- 7. 📄テスト設計の基本
- 8. 📄CloudFront + S3 + ALB で公開する(Month 5 Week 20)
- 9. 📄CLAUDE.md・プロジェクト設定
- 10. 📄PR レビュー 5 観点ルーブリック(全 Week 共通)
- 11. 📄タスク分解・仕様の書き方
- 12. 📄Playwright で E2E テスト(Month 6 Week 22)
- 13. 📄生成コードのレビュー・デバッグの勘所
- 14. 📄Trivy で脆弱性スキャン(Month 6 Week 23)
- 15. 📄CloudWatch Logs の読み方と運用(Month 6 Week 23)
- 16. 📄PDF ポートフォリオの自動生成(Month 6 Week 24)