対象読者: AWSをこれから触る人。 AWS勉強シリーズの10本目で、今回はCloudWatchです。全体の地図は索引記事にあります。
ここまでのシリーズでEC2やLambdaを動かした後、こういう場面で手が止まった人向けに書きます。
- 「昨日まで動いていたのに、今朝見たら止まっていた。何が起きたのか分からない」
- 「Lambdaが失敗してるらしいけど、エラー文をどこで見ればいいの?」
どちらもCloudWatchの「測る・知らせる・残す」の3点セットで答えが出ます。

CloudWatchを家の見守りに例えた図(筆者作成)。この記事はこの3つの道具を順番に触ります。
CloudWatchとは何か
Amazon CloudWatchは、AWS上で動いているものの状態を記録して、異常を知らせてくれる監視サービスです。上の図の通り、家の見守りセットだと思うのが近い。
- メトリクス = 温度計。CPU使用率や応答時間のような数値を、時系列でずっと記録する
- アラーム = 火災報知器。数値が決めたしきい値を超えたら知らせる
- ログ = 防犯カメラの録画。プログラムが出した文字の記録を全部残す
EC2を起動した瞬間から、CPU使用率などの基本メトリクスは頼んでいなくても記録されています。Lambdaのprint出力も自動でログに入っています。つまりCloudWatchは「導入するもの」というより、最初から動いている記録係の見方を覚えるものです。
今AWSの地図のどこにいるか
作る系のサービスは前回までで一巡して、今回から「運用する系」です。作ったものが動き続けているかを見守るのがCloudWatch、その構成をコードで再現できるようにするのが次回のCloudFormationです。
何に使うか
- 止まった原因を後から調べる: 冒頭の「今朝見たら止まっていた」は、この2段で調べる

調べ方の全体像(筆者作成)。この記事の「使い方」のコードは、この図の①と②をそのまま叩きます。
- Lambdaのデバッグ: Lambda記事で「eventをprintして形を見る」と書きました。そのprintの出力先がCloudWatch Logsです
- 異常の通知: 「応答が遅くなったらメールで知らせる」をアラーム+SNSで組む。シリーズの部品がここで繋がります
- 請求の見張り: 「気づいたら課金されていた」を防ぐ請求アラームもCloudWatchの仕事です(この記事の最後に)
今はこれだけ分かればOK
| 必須の3語 | 家で言うと | 中身 |
|---|---|---|
| メトリクス | 温度計 | 名前空間(どのアプリの)+メトリクス名(何の数値)で整理された時系列の数値 |
| アラーム | 火災報知器 | 「どのメトリクスが」「いくつを超えたら」「どうする」の3点を決めた見張り |
| ロググループ/ログストリーム | 録画の棚/テープ | ログの入れ物。グループ(アプリ単位)の中にストリーム(サーバー単位)が並ぶ |
ダッシュボード、X-Ray、CloudWatch Agentは後回しで大丈夫です。この記事にも出てきません。
このコードを動かす前提
この記事のコードはPython(boto3)です。動かす方法は2通りあります。
A. AWSアカウント無しで試す(おすすめ): motoというAWSのそっくりさんを使うと、課金もサインアップもなしでPCの中だけで動きます。
python3 -m venv venv
./venv/bin/pip install boto3 moto
from moto import mock_aws
@mock_aws # この中のboto3呼び出しは全部ローカルの偽AWSに行く
def main() -> None:
... # 以下の記事のコードをここに入れる
main()
B. 本物のAWSで動かす: 先にIAMユーザーとアクセスキーの設定が要ります。手順はIAM記事の「最初の1回だけやる手順」にあります。
使い方
「測る→知らせる→残す」の順に、3つの道具を1回ずつ触ります。
import datetime
import boto3
cw = boto3.client("cloudwatch", region_name="ap-northeast-1")
logs = boto3.client("logs", region_name="ap-northeast-1")
now = datetime.datetime.now(datetime.timezone.utc)
# ① 温度計: 応答時間を4回記録する(120ms, 95ms, 340ms, 80ms)
for i, v in enumerate([120, 95, 340, 80]):
cw.put_metric_data(Namespace="MyApp",
MetricData=[{"MetricName": "ResponseTimeMs", "Value": v,
"Timestamp": now - datetime.timedelta(minutes=5*(3-i)),
"Unit": "Milliseconds"}])
# 記録した数値を集計して取り出す
stats = cw.get_metric_statistics(Namespace="MyApp", MetricName="ResponseTimeMs",
StartTime=now - datetime.timedelta(hours=1), EndTime=now + datetime.timedelta(minutes=1),
Period=3600, Statistics=["Average", "Maximum", "SampleCount"])
dp = stats["Datapoints"][0]
print("平均:", dp["Average"], "最大:", dp["Maximum"], "件数:", dp["SampleCount"])
# -> 平均: 158.75 最大: 340.0 件数: 4.0
# ② 火災報知器: 「平均応答が300msを超えたら」の見張りを立てる
cw.put_metric_alarm(AlarmName="slow-response",
Namespace="MyApp", MetricName="ResponseTimeMs",
Statistic="Average", Period=300, EvaluationPeriods=1,
Threshold=300, ComparisonOperator="GreaterThanThreshold")
print(cw.describe_alarms(AlarmNames=["slow-response"])["MetricAlarms"][0]["StateValue"])
# -> OK (motoの場合。本物のAWSでは作成直後はINSUFFICIENT_DATAで、データが揃ってからOKかALARMに変わる)
# ③ 録画: ログの棚とテープを作って、2行書き込む
logs.create_log_group(logGroupName="/myapp/web")
logs.create_log_stream(logGroupName="/myapp/web", logStreamName="server-1")
ts = int(now.timestamp() * 1000)
logs.put_log_events(logGroupName="/myapp/web", logStreamName="server-1",
logEvents=[{"timestamp": ts, "message": "GET /photos 200 120ms"},
{"timestamp": ts + 1000, "message": "GET /photos 500 error: db timeout"}])
# 録画を「error」で絞って再生する
ev = logs.filter_log_events(logGroupName="/myapp/web", filterPattern="error")
print([e["message"] for e in ev["events"]])
# -> ['GET /photos 500 error: db timeout']
実測の出力です。4回の記録から平均158.75ms・最大340msが集計され、2行のログからerrorを含む1行だけが返ってきました。
平均: 158.75 最大: 340.0 件数: 4.0
alarm state: OK
errorで絞る: ['GET /photos 500 error: db timeout']
最後の filter_log_events が、実務で一番使う形です。「昨日の夜に何があったか」は、時刻の範囲と error や timeout のパターンで録画を絞って読みます。
今あなたが作ったもの
- 自分のアプリ用の温度計が1本(名前空間MyAppのResponseTimeMs)。集計は平均・最大・件数で取り出せる
- 「平均300ms超えたら」の火災報知器が1個(状態はOK)
- 録画の棚が1つと、その中に2行の録画。errorで絞る検索も動いた
アラームの状態は set_alarm_state で手動でALARMに切り替えられます(実測でOK→ALARMに変わりました)。本物の運用では、この切り替わりをSNSに繋いでメールを飛ばします。
実際に叩くと止まるところ
間違えたときに何が返るか、実際に叩きました。
| やったこと | 返ってきたエラー | 意味 |
|---|---|---|
| 無いロググループに書き込み | ResourceNotFoundException |
棚が先。create_log_groupしてから |
| ストリームを作らずに書き込み | ResourceNotFoundException |
棚だけでは書けない。テープ(ストリーム)も先に作る |
| 同名ロググループを再作成 | ResourceAlreadyExistsException |
作り直しは削除してから |
| 同名アラームを再作成 | エラーにならず上書き | put_metric_alarmは「作成または更新」。しきい値の変更はこれでいい |
| 無い名前空間で統計を取る | エラーにならず0件 | タイプミスしても空が返るだけ。「データが無い」と「名前を間違えた」は見分けが付かないので、名前空間の綴りを最初に疑う |
下2つが罠です。「エラーが出ない=合っている」ではないのがCloudWatchの取り扱い注意点で、特に最後の「無い名前を聞いても0件が返るだけ」は、グラフが空のときにまず疑う場所です。
似たサービスとの使い分け
| 迷うところ | 答え |
|---|---|
| メトリクスとログ、どっちに残す | 数値の推移を見たいならメトリクス、原因の文章を読みたいならログ。実務は両方で、メトリクスで「いつ」を特定→ログで「何が」を読む |
| CloudTrailとの違い | CloudWatchは「アプリの動き」の記録、CloudTrailは「誰がAWSを操作したか」の監査記録。目的が違う |
| printデバッグでよくない? | Lambdaのprintは自動でCloudWatch Logsに入る。つまりprintデバッグの読み場所がここ。対立するものではない |
料金の考え方
無料枠が常設で、基本メトリクス・アラーム10個・ログ5GB/月まではだいたい無料の範囲に収まります。課金で気をつけるのは2つ。
- ログの溜めっぱなし: ログは消さない限り保存料がかかり続ける。ロググループに保持期間(たとえば30日)を設定しておく
- カスタムメトリクスの出しすぎ: 自分で記録するメトリクスは1本あたり月0.30 USD。ループの中で毎秒違う名前のメトリクスを作る、のような事故に注意
逆に、請求アラームは最初に作る価値があります。「今月の請求額が10 USDを超えたら知らせる」をCloudWatchのアラームで組めるので、学習用アカウントの安全装置になります(請求メトリクスはバージニア北部リージョンにだけあるので、そこだけ注意)。
よくある注意点
- メトリクスの保存には遅れと粒度がある。 記録した直後の数秒は集計に出ないことがある。また古いデータほど粗い粒度に丸められていく(高解像度のまま無期限には残らない)
-
ログはグループ→ストリーム→イベントの3層。 書き込み先はストリームで、検索はグループ単位。実測の通り、どちらが無くても
ResourceNotFoundException - アラームには「データ不足」という第3の状態がある。 OK/ALARMの他にINSUFFICIENT_DATA。作った直後や、メトリクスが来なくなった時はこれになる。「アラームが鳴らない=正常」とは限らない
- リージョンに紐づく。 東京のメトリクスは東京のCloudWatchにしかない。「グラフが空」の原因の定番は、名前空間の綴りかリージョン違い
まとめ
冒頭の2つの症状に戻ります。
- 「今朝見たら止まっていた」→ メトリクスのグラフで異常が始まった時刻を特定し、その時刻のログを
filter_log_events(コンソールならログのインサイト)で絞って読む。測る道具と残す道具の2段構え - 「Lambdaのエラー文はどこ」→ CloudWatch Logsの
/aws/lambda/関数名のロググループ。printした物も全部ここ
見守りセットは3つとも、導入する前から動いています。今日覚えたのは見方だけで、グラフが空のときに疑う場所(名前空間の綴りとリージョン)ももう知っています。
次回はCloudFormation(インフラのコード化)を予定しています。
参考
- Amazon CloudWatch 公式ドキュメント
- 前回: 【図解】AWS DynamoDBとは
- シリーズ索引: AWSとは結局何なのか
