はじめに
こんにちは、KokoHakotaroです。
最近、私はあるWeb掲示板サイトを定期的にスクレイピングし、更新があったときに通知を送るというシステムを開発しました。
このシステムのキモとなるのが、システム通知を送る手段です。
AWSで通知といえば、SNS経由のメール通知が定番、しかし、大量のメールに埋もれてしまい、すぐに気づくことができないこともしばしば。
理想としているのは、スマホの通知アプリにPush通知を送るという方法でしたが、自分でそんなアプリを作れるはずもなく...と、そんなときに見つけたのが、Pushoverという通知サービスです。
今回は、このPushoverを使って、システム通知に加えてAWSの障害アラートも、まとめてスマホにPush通知してしまおう!というお話です。
通知先に使える様々なサードパーティ製アプリ
AWSの通知をスマホに送る手段には、いろいろあるかと思います。メール、チャットツールへの通知、プッシュ通知専用アプリ……などです。
CloudWatch AlarmやアプリからのイベントをSNSトピックに集約し、そこからLambdaを経由してHTTP/APIを叩けば、通知先には任意のサードパーティ製アプリを選べます。SlackやLINEなどのチャットツールへのWebhook通知も同じ仕組みで実現できますが、これらは業務連絡や他のメッセージと同じ画面に表示されるため、埋もれてしまうという点ではメール通知と同じ課題を抱えています。
そこで今回は、冒頭で触れたPushoverを採用しました。
Pushoverとは
Pushoverは、スマホにプッシュ通知を送れるサービスです。
- HTTPS APIを叩くだけで通知を送信できる
- 専用の
@pomail.netアドレス宛にメールを送っても通知に変換される(Eメールゲートウェイ) - 送信元ごとに
Application Tokenを発行でき、アイコン・アプリ名で見分けられる
-
priorityで緊急度を制御できる - 1つのアカウントに複数端末(スマホ・タブレット・デスクトップ等)を登録でき、同じ通知を同時に受け取れる
- 料金は買い切り制(詳細は後述の「運用コスト」)
- 無料で送れるのは1アカウントにつき月10,000件まで。この上限はアカウント全体で共有するので、アプリを増やしても送れる量は増えない
Pushoverは通知専用アプリなので、届く内容が常に「通知」だけ。他の情報に紛れることなく一目で気づけるというのが、数あるサードパーティ製アプリの中でもPushoverを選んだ決め手でした。
構成解説
-
CloudWatch Alarm:監視対象Lambdaのメトリクス(
Errors等)の状態遷移(OK→ALARM等)を検知 -
SNS(単一トピック):Alarmの
alarm_actionsから発行されたメッセージを受け取る。このトピックは複数プロジェクトで共用し、監視したいLambda側からalarm_actionsでこのトピックARNを指定するだけで乗っかれるようにしてある -
Lambda(中継Bot本体):SNSメッセージを受け取り、CloudWatch Alarm形式かどうかを判定して整形。該当すれば「[状態] アラーム名」+理由を日本語で読みやすく組み立て、非該当なら
Subject/Messageをそのまま中継するフォールバックを持たせています -
SSM Parameter Store:Pushoverのuser key(受信先)と、中継Bot自身が使う既定のApplicationトークンを
SecureStringで保管。送信元ごとのトークンは中継側には置かず、各送信元プロジェクトが自分の名前空間で管理します(後述) - Pushover:最終的にスマホへプッシュ通知。送信元システムが自分用のトークンの在り処をSNSメッセージに添えて送り、中継Lambdaはそれを読んで送信します。Pushoverアプリ内では送信元ごとにアイコン・アプリ名が分かれるので、「アプリ本来の通知」と「インフラの異常」が混ざって見落とすリスクを避けられます
なぜ中継Lambdaを挟んだか
CloudWatch Alarmのalarm_actionsはSNSトピックへの発行が定番ですが、その先の選択肢はいくつかありました。
一番手軽なのは「SNS→Eメール(Pushoverのメール転送アドレス宛)」でLambdaすら要らない構成です。ただこれだと、CloudWatch Alarmの機械的な英語文面(ALARM: "関数名" in ap-northeast-1)がそのまま届いてしまい、内容を整形できません。
日本語で読みやすく、かつ状態に応じて優先度(priority)も制御したかったので、SNS→Lambda→Pushoverという中継構成を選びました。Lambdaを挟むことで、CloudWatch Alarm以外の任意のSNSメッセージにも対応できる汎用性も手に入りました。
実際の整形ロジックはこんな感じです。CloudWatch Alarm形式(AlarmName・NewStateValueを含むJSON)を検知したら、状態に応じてタイトルと優先度を組み立てます。
def _build_cloudwatch_alarm_notification(parsed: dict) -> dict:
alarm_name = parsed.get("AlarmName", "(不明なアラーム)")
new_state = parsed.get("NewStateValue", "UNKNOWN")
title = _truncate(f"[{new_state}] {alarm_name}", PUSHOVER_TITLE_LIMIT)
body_lines = []
if parsed.get("AlarmDescription"):
body_lines.append(parsed["AlarmDescription"])
if parsed.get("NewStateReason"):
body_lines.append(parsed["NewStateReason"])
if parsed.get("StateChangeTime"):
body_lines.append(f"発生時刻: {parsed['StateChangeTime']}")
message = "\n".join(body_lines) if body_lines else "(詳細情報なし)"
message = _truncate(message, PUSHOVER_MESSAGE_LIMIT)
priority = 1 if new_state == "ALARM" else 0
return {"title": title, "message": message, "priority": priority}
CloudWatch Alarm形式に該当しない場合は、Subject/Messageをそのまま中継するフォールバックに回します。
実際の通知例
実際に監視対象のLambdaでエラーが発生すると、こんな通知がスマホに届きます。
タイトルの[ALARM]とアラーム名、本文の理由と発生時刻は、いずれも中継Lambdaが組み立てたものです。CloudWatch Alarmがそのまま送ってくる英語の文面と違い、通知を開かなくてもロック画面で「何がどうなったか」が読み取れます。
複数プロジェクトから使い回せるようにする
Webスクレイピングアプリ以外のLambdaが増えても対応できるように、という部分は設計上もう少し工夫しています。
- SNSトピックは単一のものを共用し、監視したいLambda側は
alarm_actionsにこのトピックARNを指定するだけで乗れるようにする - トピックARNはSSM Parameter Store(
String型、非シークレット)に置いて、消費側がそこから参照できるようにする
ARNを手元に控えておく案も考えましたが、トピックを作り直した際にARNが変わると気付かれずに壊れるリスクがあります。SSM経由にしておけば、消費側は常に最新のARNを拾えます。
送信元をどう見分けるか
送信元ごとにPushoverのアプリを分けるといっても、中継側で送信元の一覧を管理するような作りにはしていません。通知を送るシステムが、自分用のトークンを入れたSSM Parameter StoreのARNをメッセージに添えて送る、それだけです。中継Lambdaは受け取ったARNのパラメータを読み、そこに入っているApplicationトークンで通知を送ります。
ARNを載せる場所は、SNSのメッセージ属性(MessageAttributes)にしました。本文に混ぜる案もありましたが、中継Lambdaは判定に引っかからないメッセージを本文そのまま転送するので、それだとARNの文字列が通知の見た目に出てきてしまいます。
送信元は、publish時に属性を1つ足すだけです。
sns.publish(
TopicArn=topic_arn,
Subject="お知らせが更新されました",
Message=body,
MessageAttributes={
"pushover_token_parameter": {
"DataType": "String",
"StringValue": "arn:aws:ssm:ap-northeast-1:<account-id>:parameter/<プロジェクト名>/pushover_token",
}
},
)
受け取る側は、属性があればそのパラメータを読み、無ければ既定のトークンを使います。
def resolve_app_token(sns, default_token, cache):
arn = extract_token_parameter_arn(sns)
if arn is None:
return default_token
if arn in cache:
return cache[arn]
name = resolve_token_parameter_name(sns)
if name is None:
logger.warning(...) # 検証を通らないARN
cache[arn] = default_token
return default_token
try:
token = get_parameter(name)
except Exception:
logger.warning(...) # パラメータが読めない
token = default_token
cache[arn] = token
return token
この形なら、新しいシステムが増えても「Pushoverでアプリを登録し、トークンをSSMに置き、そのARNを添えて送る」だけで通知に乗れます。中継側のコードも設定も触る必要がありません。ARNが添えられていない通知は、既定のトークンでそのまま届きます。CloudWatch Alarmのalarm_actionsはトピックARNを指定するだけで送信側が情報を足せないため、アラート起点の通知はこの既定ルートに乗ります。
「送信元が指定したARNを中継側が読む」と書くと、なんでも読めてしまいそうに見えますが、中継LambdaのIAMで許可しているのは arn:aws:ssm:<region>:<account>:parameter/*/pushover_token だけです。ワイルドカードは挟むものの、末尾が/pushover_tokenであることを命名規約にして、トークン用のパラメータ以外は読めないようにしてあります。Lambda側でも、SSMを呼ぶ前に「同じリージョン・同じアカウントか」「命名規約に合っているか」を検証しています。
そして、どの段階で失敗しても既定のトークンで送るようにしてあります。ここはアラートの通知経路そのものなので、「アプリ名が想定と違う通知が届く」より「通知が届かない」ほうが明確に困るからです。
なお、Pushoverのuser keyは「どのアカウントで受け取るか」を指す値なので、送信元ごとに変えるのはApplicationトークンだけです。
運用コスト
| リソース | 概算コスト |
|---|---|
| Lambda(実行) | $0(無料枠内) |
| SNS(発行・配信) | $0(無料枠内) |
| SSM Parameter Store | $0 |
| CloudWatch Logs | $0(無料枠内) |
| Pushover Application追加登録 | $0(登録自体は無料) |
| Pushover買い切り費用 | $4.99(初回のみ。2ヶ月目以降は0円) |
個人利用の規模であれば、AWS側はほぼ$0/月で運用できています。なお買い切り費用はプラットフォーム単位で、iOS・Android・デスクトップをまたいで使う場合はそれぞれ$4.99かかります(同じプラットフォームなら複数端末でも1回の購入でカバーされます)。
まとめ
「メールよりスマホですぐ気付きたい」という個人的な小さな動機から始まって、気付けば「個人のAWSアカウント全体のアラート中継基盤」になりました。
- Pushoverは個人開発システム向きの通知アプリ。通知専用なので、メールやチャットツールと違って他の情報に埋もれない
- 中継Lambda1つで、複数システムの通知を$0で一括管理できる
- 複数プロジェクトから使い回すなら、単一SNSトピック+SSM Parameter Store経由でのARN公開が個人利用規模にはちょうど良い


