前回ハードウェアウォッチドッグを入れて安心していたのですが、数日後にまったく同じ症状でまたフリーズしました。
目次
ウォッチドッグを入れたのに、また止まった
LED消灯、リセットボタン無反応、USBの抜き差し(完全な電源断→再投入)でのみ復帰。前回とまったく同じ症状です。AWS側のログを確認すると、今度は46時間もの間、API Gatewayへのリクエストが一切ありませんでした。
ウォッチドッグが正しく機能していれば、120秒ごとに再起動を試みているはずです。それなのに46時間まったく音沙汰がないのは、様子がおかしいと思いました。
電源品質を疑う理由
考えてみると、ウォッチドッグは「CPUが正常に動いていること」が前提の仕組みです。もし電源そのものが不安定で、CPUが正常に起動すらできない状態(ブラウンアウト)に陥っているとしたら、ソフトウェアのウォッチドッグは何の役にも立ちません。
使っていた電源を確認すると、Ankerの電源タップ(USB-Aポート、単独使用時5V/2.4A)の他のポートに別の機器を挿していたこと、ESP32付属の安価なUSBケーブルを使っていたことが分かりました。ケーブルの芯線が細いと、Wi-Fi送信時の瞬間的な電流ピーク(200〜300mA)でケーブル内の電圧降下が大きくなり、電圧不足を起こすことがあります。ケーブルを交換してMacに接続し直したところ、今のところ安定していますが、まだ数日しか経っておらず、これで解決したと言い切るのは早いと感じています。
原因究明より先に「気づく仕組み」
電源の問題だとすれば根本対策(電源アダプタの見直し、電源回路にコンデンサを足すなど)が必要になりますが、それを詰め切る前に、もっと基本的な問題に気づきました。今回もまた、数日後にダッシュボードを見て初めて気づいたということです。原因が特定できていなくても、止まったことにすぐ気づければ被害は最小限にできます。屋外設置後はなおさらです。
というわけで、原因究明と並行して、CloudWatch Alarm から Slack に通知する仕組みを先に作ることにしました。
構成: CloudWatch Alarm → SNS → Lambda → Slack
CloudWatch Alarm(データ停止を検知)
│
▼
SNS Topic
│
▼
Lambda(Python)
│
▼
Slack Incoming Webhook
Slack連携の方法は2つ検討しました。
| 方式 | 内容 | 採否 |
|---|---|---|
| AWS Chatbot | AWS公式のSlack連携サービス | 見送り。Terraformだけでは完結せず、AWSコンソールでSlackワークスペースを手動連携する一手間が必要 |
| Lambda + Incoming Webhook | 自作の橋渡しLambdaがSlackのWebhook URLにPOSTする | 採用。全てTerraformで完結する |
これまでの記事で一貫してTerraformだけで完結する構成にこだわってきたので、後者を選びました。Slack側は事前にIncoming Webhook URLを発行しておくだけで、AWS側の作業は不要です。
Lambdaの中身はシンプルです。SNSから届いたメッセージ(CloudWatch Alarmの状態変化通知)をパースして、Slackの書式に整形してPOSTするだけです。
def handler(event, context):
for record in event["Records"]:
message = json.loads(record["Sns"]["Message"])
text = f"{emoji} *{message['AlarmName']}*: {message['NewStateValue']}\n{message['NewStateReason']}"
# Slack Incoming Webhook に POST
通知を読みやすくする
最初のバージョンでは、CloudWatch から届く時刻がそのまま(UTC)表示されていました。日本時間で運用しているのに毎回頭の中で+9時間する羽目になるので、Lambda側でJSTに変換してから送るようにしました。
def _format_jst(iso_str):
dt = datetime.datetime.strptime(iso_str, "%Y-%m-%dT%H:%M:%S.%f%z")
jst = dt.astimezone(datetime.timezone(datetime.timedelta(hours=9)))
return f"{jst.year}/{jst.month}/{jst.day} {jst.strftime('%H:%M:%S')}"
あわせて、通知を見てすぐダッシュボードを開けるよう、CloudWatch ダッシュボードへのリンクも本文に追加しました。リンク先はTerraformの出力(outputs.tf)と同じ組み立て方をLambdaの環境変数に渡しているだけです。
environment {
variables = {
DASHBOARD_URL = "https://${var.aws_region}.console.aws.amazon.com/cloudwatch/home?region=${var.aws_region}#dashboards:name=${aws_cloudwatch_dashboard.sensor.dashboard_name}"
}
}
通知を受け取ってから「今どうなってる?」を確認するまでのワンステップが減り、地味に便利です。
ハマったところ
症状: CloudWatch Alarmを作って様子を見ていたが、データが本当に止まってもアラームが発火しない気配がある(テストで意図的にデータを止められるわけではないので確信は持てないが、ドキュメントを読み直して気づいた)。
原因: CloudWatchのアラームは、デフォルトでは「メトリクスのデータが無い」状態をINSUFFICIENT_DATAという別の状態として扱い、アラームとしては扱わない。今回のように「センサーからのデータが完全に途絶える」ことを検知したい場合、単に閾値判定するだけでは不十分だった。
解決策: treat_missing_data を breaching に明示的に設定する。これで「データが無いこと自体」がアラーム条件を満たしている(=閾値を超えている)ものとして扱われる。
resource "aws_cloudwatch_metric_alarm" "sensor_stopped" {
metric_name = "Temperature"
statistic = "SampleCount"
period = 300
evaluation_periods = 3
threshold = 1
comparison_operator = "LessThanThreshold"
treat_missing_data = "breaching" # これが無いと「データ無し」を検知できない
}
気づきにくい理由: 「閾値を下回ったらアラーム」という設定だけを見ると正しく動きそうに見える。しかし閾値判定はメトリクスの値が存在する前提の話で、値そのものが存在しない(欠測)場合の扱いは別のパラメータで制御する必要がある、という点が直感的に分かりにくい。
動作確認
SNSに手動でテストメッセージを送り、Lambda経由でSlackに通知が届くことを確認しました。また、アラームを作成した直後の初回評価(データは正常に来ているのでOK状態になる)でも、実際に自動で通知が飛ぶことを確認できました。
まとめ
| 項目 | 内容 |
|---|---|
| 検知対象 | センサーのTemperatureメトリクスが15分(5分×3回)以上0件 |
| 通知経路 | CloudWatch Alarm → SNS → Lambda → Slack Incoming Webhook |
| 通知内容 | JST変換した時刻、ダッシュボードへのリンクを含める |
| ハマった点 | treat_missing_dataをbreachingにしないと欠測を検知できない |
| 根本原因 | 未特定(電源品質を疑っているが検証中) |
根本原因はまだ解決していませんが、少なくとも「数日気づかない」という最悪の事態は避けられるようになりました。電源側の検証は引き続き続けます。
次回
電源品質の検証(Mac接続での安定稼働がどれだけ続くか)を続けつつ、発注済みの電源部材が届き次第、ブレッドボードでの電源回路の検証にも進みます。