Skip to content
まことの手帳
Go back

ESP32 でセンサーデータを AWS に送る #11 アラーム通知編

前回ハードウェアウォッチドッグを入れて安心していたのですが、数日後にまったく同じ症状でまたフリーズしました。

目次

ウォッチドッグを入れたのに、また止まった

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 ChatbotAWS公式の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接続での安定稼働がどれだけ続くか)を続けつつ、発注済みの電源部材が届き次第、ブレッドボードでの電源回路の検証にも進みます。

参考リンク


Share this post on:

Previous Post
ESP32 でセンサーデータを AWS に送る #10 ウォッチドッグ編