モニタリング — CloudWatch アラームと X-Ray
CloudWatch Logs Insights の運用クエリ、ECS / RDS / ALB の核となるメトリクスとアラーム閾値、SNS → Slack 通知、X-Ray 分散トレースで遅いリクエストをすぐ把握するところまで。運用の目をともす流れを整理します。
第22章 ~ 第25章 でインフラがコードになり、デプロイが自動になりました。ところが肝心のこのシステムがうまく動いているか — 5xx が増えたか、RDS CPU が 80% か、どのリクエストが5秒かかったか — を1画面で見られていません。
本章はその状態をすぐ見えるようにします。4部の五番目の章として、扱う内容は次のとおりです。
- CloudWatch Logs + Logs Insights の運用クエリ
- CloudWatch Metrics — ECS / RDS / ALB の核となるメトリクスとアラーム閾値
- アラーム → SNS → Slack の流れ
- X-Ray — 分散トレースで「どこが遅いのか」をすぐ把握
- ダッシュボード — 一つの画面でシステムの状態
大きな絵 — モニタリングの4つの構成 #
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Metrics │ Logs │ Traces │ Events │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ "どれだけ" │ "何が" │ "どこで" │ "いつ" │
│ リクエスト数, 5xx │ stacktrace │ DB が5秒 │ デプロイ, スケール │
│ CPU, メモリ │ access log │ 外部 API 1秒 │ フェイルオーバー │
├──────────────┼──────────────┼──────────────┼──────────────┤
│ CloudWatch │ CloudWatch │ X-Ray │ EventBridge │
│ Metrics │ Logs │ │ │
└──────────────┴──────────────┴──────────────┴──────────────┘本章は Metrics + Logs + Traces の三領域です。Events は 第19章 EventBridge / SQS / SNS で扱いました。
1) CloudWatch Logs — すでに流れている #
第22章 の Task Definition に awslogs が含まれているので、すべてのコンテナの stdout/stderr が自動で CloudWatch Logs へ行きます。
Log Group: /ecs/blog-api
│
├── Log Stream: api/<task-id-1> ← Task ごとに stream 一つ
├── Log Stream: api/<task-id-2>
└── Log Stream: api/<task-id-3>Retention 設定 — コストの分離 #
デフォルトは無期限保管です。小さなトラフィックでも1か月分のログが GB 単位で積もるとコストが大きくなります。
aws logs put-retention-policy \
--log-group-name /ecs/blog-api \
--retention-in-days 30推奨値は次のとおりです。
- 運用 access log: 30 ~ 90日
- デバッグ / verbose: 7日
- 監査ログ: 365日(または S3 export 後に削除)
構造化ログが核心 #
print() は検索が難しいです。JSON で出力すれば Logs Insights がキーごとにクエリできます。
import logging, json, sys
class JsonFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
"ts": self.formatTime(record),
**getattr(record, "extra", {}),
})
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
logging.basicConfig(level=logging.INFO, handlers=[handler])@app.middleware("http")
async def access_log(request, call_next):
start = time.time()
response = await call_next(request)
logging.info("access", extra={"extra": {
"method": request.method,
"path": str(request.url.path),
"status": response.status_code,
"duration_ms": int((time.time() - start) * 1000),
"request_id": request.state.request_id,
}})
return responseこのように出力すると、Logs Insights が次のようなクエリに正確に答えます。
2) Logs Insights — 運用クエリ7個 #
よく使うクエリのまとめです。ブックマークしておくとよいです。
A) 5xx だけ抽出 #
fields @timestamp, status, path, request_id, message
| filter status >= 500
| sort @timestamp desc
| limit 100B) 応答時間の分布 (p50/p90/p99) #
fields @timestamp, duration_ms
| filter ispresent(duration_ms)
| stats
count(*) as requests,
pct(duration_ms, 50) as p50,
pct(duration_ms, 90) as p90,
pct(duration_ms, 99) as p99
by bin(5m)C) 最も遅い path #
fields path, duration_ms
| filter duration_ms > 1000
| stats count(*), avg(duration_ms), max(duration_ms) by path
| sort avg(duration_ms) desc
| limit 20D) request_id で一つのリクエストを追跡 #
fields @timestamp, level, message, path, status, duration_ms
| filter request_id = "abc-123-xyz"
| sort @timestamp ascE) Stacktrace が出力された行 #
fields @timestamp, message
| filter @message like /Traceback|exception/
| sort @timestamp descF) ログイン試行 (認証失敗) #
fields @timestamp, source_ip, username
| filter event = "auth_fail"
| stats count(*) by source_ip
| sort count(*) descG) コスト — どの path が最も多く呼び出されたか #
fields path
| stats count(*) by path
| sort count(*) desc
| limit 30Saved Queries #
よく使うクエリはコンソールで保存してチーム全体で共有します。CloudFormation / Terraform で IaC 化できます(aws_cloudwatch_query_definition)。
3) CloudWatch Metrics — 核となる指標 #
ECS Container Insights #
デフォルトの ECS メトリクスは貧弱です。Container Insights をオンにすると、task / service 単位の CPU / メモリ / ネットワーク / ディスク / running task 数をまとめて見られます。
aws ecs update-cluster-settings \
--cluster blog-cluster \
--settings name=containerInsights,value=enabled追加コスト(小さなクラスターで ~$1 ~ 3/月)がかかりますが、運用では必須です。
モニタリング表 — 何を見るべきか #
| メトリクス | 資源 | 意味 | アラーム閾値 (例) |
|---|---|---|---|
HTTPCode_Target_5XX_Count | ALB | バックエンド 5xx | 5分合計 ≥ 5 |
HTTPCode_ELB_5XX_Count | ALB | ALB 自体の 5xx(大半は healthy host 0) | 5分合計 ≥ 1 |
TargetResponseTime (p99) | ALB | 応答時間 p99 | 5分平均 ≥ 1.0s |
UnHealthyHostCount | Target Group | 死んだ task 数 | 5分平均 ≥ 1 |
CPUUtilization (Service) | ECS | サービス平均 CPU | 5分平均 ≥ 80% |
MemoryUtilization (Service) | ECS | メモリ | 5分平均 ≥ 85% |
RunningTaskCount | ECS | 立ち上がっている task 数 | desired と異なる |
CPUUtilization | RDS | DB CPU | 5分平均 ≥ 80% |
DatabaseConnections | RDS | 接続数 | max_connections の 80% |
FreeStorageSpace | RDS | 残りのディスク | < 5GB |
ReadLatency / WriteLatency | RDS | ディスク遅延 | > 50ms |
Custom Metrics #
アプリから直接出力するメトリクスです。ログに EMF (Embedded Metric Format) で埋め込めば、別途呼び出しなしでメトリクスが作られます。
import json, time, logging
def emit_metric(metric_name, value, unit="Count", **dims):
payload = {
"_aws": {
"Timestamp": int(time.time() * 1000),
"CloudWatchMetrics": [{
"Namespace": "BlogApp",
"Dimensions": [list(dims.keys())],
"Metrics": [{"Name": metric_name, "Unit": unit}],
}],
},
metric_name: value,
**dims,
}
logging.info(json.dumps(payload))
emit_metric("PostCreated", 1, env="prod")
emit_metric("CommentCreated", 1, env="prod")
emit_metric("LoginFailed", 1, source_ip="...")CloudWatch がログをパースして自動で BlogApp/PostCreated メトリクスを作ります。別途 PutMetricData API 呼び出しがないので、コストと遅延の両方を節約します。
4) アラーム — 閾値を超えると人を呼ぶ #
aws cloudwatch put-metric-alarm \
--alarm-name "blog-alb-5xx-burst" \
--metric-name HTTPCode_Target_5XX_Count \
--namespace AWS/ApplicationELB \
--statistic Sum \
--period 60 \
--evaluation-periods 5 \
--datapoints-to-alarm 3 \
--threshold 5 \
--comparison-operator GreaterThanOrEqualToThreshold \
--treat-missing-data notBreaching \
--dimensions Name=LoadBalancer,Value=app/blog-alb/abc123 \
--alarm-actions arn:aws:sns:ap-northeast-2:123456789012:ops-alerts核となるオプションを整理すると次のとおりです。
| オプション | 意味 |
|---|---|
period | データポイントの単位(60 = 1分) |
evaluation-periods | 何個のポイントを評価するか |
datapoints-to-alarm | そのうち何個が閾値を超えたらアラーム |
treat-missing-data | データがないとき — notBreaching 推奨 |
comparison-operator | >= / > / < / <= |
5/3 パターン(「直近5分のうち3分が閾値を超えたら」)が、一時的なスパイクをふるい落としつつ本当の事故を掴む基準です。
Composite Alarm #
複数のアラームをまとめます。「ALB 5xx アラーム AND task running 正常」なら本当のバックエンドの問題です。
aws cloudwatch put-composite-alarm \
--alarm-name "blog-real-incident" \
--alarm-rule "ALARM('blog-alb-5xx-burst') AND OK('blog-running-tasks-low')"OK() は普段は ok なのに別の一つのアラームが alarm である場合を意味し、ノイズを減らします。
5) SNS → Slack — 人に届く部分 #
CloudWatch Alarm
│
▼
SNS Topic (ops-alerts)
│
├── Email subscription (運用陣)
├── SMS subscription (oncall)
├── Lambda subscription ← Slack webhook へ変換
└── PagerDuty / OpsGenieSNS → Slack Lambda #
import json, os, urllib.request
WEBHOOK = os.environ["SLACK_WEBHOOK"]
def handler(event, context):
for record in event["Records"]:
msg = json.loads(record["Sns"]["Message"])
text = (
f":rotating_light: *{msg['AlarmName']}*\n"
f"Region: {msg['Region']}\n"
f"State: {msg['NewStateValue']} (was {msg['OldStateValue']})\n"
f"Reason: {msg['NewStateReason']}\n"
)
req = urllib.request.Request(
WEBHOOK,
data=json.dumps({"text": text}).encode(),
headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req)第17章 Lambda 基礎 のパターンです。SNS subscription で SNS が Lambda を呼び出せば終わりです。
アラームメッセージの様式 #
良いアラームメッセージには次が入ります。
- 何が壊れたか(alarm name)
- どれだけ壊れたか(threshold / actual value)
- どこで(region, service)
- いつ(timestamp)
- リンク — コンソール / ダッシュボード / Logs Insights へすぐに
リンクが最も重要です。深夜3時に Slack を見た oncall が、クリック一つでコンテキストに入れなければなりません。
6) X-Ray — 分散トレース #
「5xx が増えた」までは Metrics が知らせます。「なぜ 5xx が増えたのか?」は Logs が答えます。「このリクエストがどこで5秒を費やしたのか?」は X-Ray が答えます。
Request: POST /posts 4.2s
│
├── ALB 0.01s
│
└── ECS api 4.15s
│
├── auth.verify_token 0.05s
│
├── db.posts.insert 3.80s ← 犯人
│ └── RDS PostgreSQL 3.78s
│ └── (slow query)
│
└── notify.publish (SNS) 0.30s
└── SNS:Publish 0.28sFastAPI/Django 統合 #
pip install aws-xray-sdkfrom aws_xray_sdk.core import xray_recorder
from aws_xray_sdk.ext.fastapi.middleware import XRayMiddleware
from aws_xray_sdk.ext.sqlalchemy.query import XRayQuery
xray_recorder.configure(service="blog-api")
app = FastAPI()
app.add_middleware(XRayMiddleware, recorder=xray_recorder)
# SQLAlchemy 追跡
from aws_xray_sdk.ext.sqlalchemy_core import unpatch
# (engine 生成時に自動 patch)Sidecar — X-Ray Daemon #
ECS では X-Ray Daemon コンテナを同じ task definition の中に sidecar として置きます。
{
"containerDefinitions": [
{ "name": "api", ... },
{
"name": "xray-daemon",
"image": "public.ecr.aws/xray/aws-xray-daemon:latest",
"portMappings": [{ "containerPort": 2000, "protocol": "udp" }],
"essential": false
}
]
}アプリは 127.0.0.1:2000 へ trace を送り、daemon が X-Ray サービスへ一括送信します。別途 IAM アクション(xray:PutTraceSegments)が task role に必要です。
どこで価値が最も大きいか #
| 状況 | X-Ray の価値 |
|---|---|
| 単一コンテナ + 単一 DB | 普通 — Logs だけでも十分 |
| 複数のマイクロサービス呼び出し | 非常に大きい — どの段階が遅いかすぐ分かる |
| 外部 API 依存 | 非常に大きい — 外部が本当に遅いか検証 |
| Lambda + DynamoDB | 非常に大きい — Lambda コールドスタート、外部呼び出しの分離 |
サンプリング #
すべてのリクエストを追跡するとコストが大きくなります。サンプリングルールで 5 ~ 10% だけ追跡します。
{
"version": 2,
"rules": [{
"description": "Default",
"service_name": "*",
"http_method": "*",
"url_path": "*",
"fixed_target": 1,
"rate": 0.05
}],
"default": { "fixed_target": 1, "rate": 0.05 }
}/health のようなヘルスチェックは 0% に抜いてしまわないと、trace がノイズで埋め尽くされます。
7) ダッシュボード — 一つの画面 #
CloudWatch Dashboard に運用のシグナルを一つの画面に集めます。
[1] Requests/s (ALB) [2] 5xx rate (ALB) [3] p99 latency
[4] ECS CPU (Service) [5] ECS Memory [6] Running tasks
[7] RDS CPU [8] RDS Connections [9] RDS FreeStorageresource "aws_cloudwatch_dashboard" "blog" {
dashboard_name = "blog-overview"
dashboard_body = jsonencode({
widgets = [
{
type = "metric",
x = 0, y = 0, width = 8, height = 6,
properties = {
metrics = [["AWS/ApplicationELB", "RequestCount", "LoadBalancer", "app/blog-alb/abc123"]],
period = 60, stat = "Sum", region = "ap-northeast-2",
title = "Requests/min"
}
}
# ... あと 8 個
]
})
}定期レビュー #
毎週一度 oncall がダッシュボードを見渡しながら、徐々に悪くなる指標を探します。アラームは即時の事故だけを掴み、徐々に悪化するものは人の目のほうが速いです。
落とし穴 — 運用モニタリングの落とし穴 #
1) Alert Fatigue — アラームが多すぎる #
アラームが一日30個ならすぐに全員がアラームを無視します。推奨等級は次のとおりです。
| アラーム等級 | 頻度 | チャネル |
|---|---|---|
| Critical | 月 1 ~ 2回 | PagerDuty / SMS |
| Warning | 週 1 ~ 2回 | Slack #ops |
| Info | 頻繁に | Slack #ops-info(静かなチャネル) |
本当に人を起こすアラームは 5個未満に保ちます。
2) Logs が無限に大きくなる #
Retention 設定を欠落させると6か月後に請求書ショックが来ます。すべての log group に retention を適用します(Terraform で一括で)。
3) Logs が小さすぎる #
運用事故の直後に「あのときのログを見よう」とするのに、7日 retention ですでに消えている場合です。事故発生後すぐの export では遅すぎます。 核となるグループは30日以上に置きます。
4) X-Ray 100% サンプリング #
コストが暴走します。5 ~ 10% サンプル + エラー / 遅いリクエストのみ 100%(X-Ray の sampling rule で可能)に置きます。
5) SLO なしでアラームだけ #
アラームの閾値がどこから来たか — 「私が 80% と言いました」 — になります。SLO(例: p99 < 500ms を 99% の時間)が明示されていないと閾値が恣意的になります。SLO の定義からアラーム閾値を導きます。
6) Dashboard はあるが見ない #
作っておいて見ないダッシュボードはないのと同じです。週次 oncall 会議に30分のダッシュボードレビューを入れます。
7) アラームが人に届かない #
メールだけ使うと受信トレイの奥深くに入ります。SMS / PagerDuty / Slack の mention のような自分を呼ぶチャネルを使います。
練習問題 #
- モニタリングの4つの構成(Metrics / Logs / Traces / Events)がそれぞれどんな問い(「どれだけ / 何が / どこで / いつ」)に答えるかを §「大きな絵」を見ずに書いてみてください。本章がそのうちどの三領域を扱うかも示しておいてください。
- ALB 5xx アラームの
period、evaluation-periods、datapoints-to-alarmの三つの値がどう5/3パターンを作るかを §「アラーム」を根拠に説明し、このパターンが一時的なスパイクをふるい落とす理由を一文で書いてみてください。 - X-Ray の価値が最も大きい状況を二つ §「どこで価値が最も大きいか」の表から選び、100% サンプリングがなぜ危険かを 第27章 コスト最適化 と結びつけて説明してみてください。
一行まとめ: 可観測性は Metrics(どれだけ)・Logs(何が)・Traces(どこで)・Events(いつ)に分かれる。awslogs で自動収集されるログは構造化 JSON で出力して Logs Insights でクエリし、Container Insights で ECS/RDS/ALB のメトリクスを見る。アラームは
period × evaluation × datapointsパターンでノイズをふるい落とし、SNS → Lambda → Slack で人に届かせ、X-Ray は分散トレースで遅い段階を一行で掴みつつサンプリングでコストを制御する。
次の章 #
これでシステムがうまく動き、事故が起きればアラームが鳴る構造まで来ました。最後に — どれだけかかっているか、そしてそのコストをどう減らすかです。次の 第27章 コスト最適化とダッシュボード では、Cost Explorer 分析、Savings Plans / Spot Fargate / Graviton、Right Sizing、タグ付けの強制、コストダッシュボードまで整理し、4部を締めくくります。