はじめに
本記事では、Amazon ElastiCache のイベント通知を Amazon SNS で受け取り、Lambda 経由で PagerDuty にインシデントとして起票する構成を、実際の設定手順とあわせて紹介します。
ElastiCache のイベントについて
ElastiCacheのイベントを通知するには、Amazon SNSでの通知と、EventBridgeでの通知が利用できます。
ただ、EventBridge に届く ElastiCache イベントの一覧は以下のみで、ElastiCache のフェイルオーバーイベントは含まれていません。そのため、今回は Amazon SNS で通知する構成を組んでいきます。
※ EventBridge に届く ElastiCache イベントの一覧
- Cache Created / Cache Creation Failed / Cache Deleted
- Cache Updated / Cache Update Failed
- Cache Limit Approaching
- Snapshot Created / Snapshot Creation Failed
- Snapshot Export Failed / Snapshot Copy Failed
全体構成
通知が届くまでの流れは次のとおりです。
[ElastiCache]
│
▼
[SNS トピック]
│
└─(サブスクリプションフィルター)→ [Lambda] → [PagerDuty Events API v2]
今回、 Amazon SNS のサブスクリプションフィルターで必要なイベントだけを絞り込んでいます。ElastiCache はクラスター作成・削除・スケーリング・メンテナンスなど、あらゆるイベントを同じトピックに発行してくるため、フィルターなしでは PagerDuty に多数のインシデントが通知される可能性があります。
また、Amazon SNS から PagerDuty に直接通知することも可能ですが、通知タイトルが「AWS Notification Message」となり分かりづらいので、Lambdaを使用してタイトルを編集しています。( PagerDuty の Event Orchestrationでもフィルタリングやインシデントタイトルの編集ができますが、今回は使用しません。)
前提条件
- PagerDuty 側でサービスを作成し、Events API v2 のインテグレーションキー(
routing_key)を払い出し済み - SNS トピック / Lambda 関数 / IAM ロール / Secrets Manager シークレットを作成できる IAM 権限を保有している
検知対象のイベント
| イベント名 | 概要 |
|---|---|
ElastiCache:FailoverComplete |
レプリカノードへのフェイルオーバーが完了(Valkey / Redis OSS のみ) |
手順
① SNS トピックの作成
- [Amazon SNS] → [トピック] → [トピックの作成] を押下
- 以下を設定してトピックを作成します
| 項目 | 値 | 備考 |
|---|---|---|
| タイプ | スタンダード | — |
| 名前 | 例) elasticache-event |
— |
| 表示名 | 例) ElastiCache Event |
— |
| 暗号化 | 無効 | 有効にすると ElastiCache から発行できません |
トピックを作る際の制約がいくつかあります。いずれも AWS ドキュメントに明記されているものです。
- 暗号化されたトピックには発行できない — KMS で暗号化を有効にすると、ElastiCache 側からの発行が失敗します
- 1 つのクラスターに設定できるトピックは 1 つだけ — 通知先を分けたい場合は、トピックの先でサブスクリプションを分岐させます
- トピックはクラスターと同一リージョン・同一アカウントである必要がある
3. 作成したトピックの [アクセスポリシー] を編集し、ElastiCache からの発行を許可します
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowElastiCachePublish",
"Effect": "Allow",
"Principal": {
"Service": "elasticache.amazonaws.com"
},
"Action": "SNS:Publish",
"Resource": "arn:aws:sns:<リージョン>:<AWSアカウントID>:<トピック名>",
"Condition": {
"StringEquals": {
"aws:SourceOwner": "<AWSアカウントID>"
}
}
}
]
}
既定のアクセスポリシー(アカウント内からの各種操作を許可するステートメント)は残したまま、上記ステートメントを追加してください。
② ElastiCache 側で SNS 通知を有効化
- [ElastiCache] → 対象のクラスター(レプリケーショングループ)を選択 → [変更] を押下
- [Amazon SNS 通知] の [トピック] で、①で作成した SNS トピックを選択
- [変更をプレビュー] → [変更] で反映

③ PagerDuty の認証情報を Secrets Manager に登録
PagerDuty のインテグレーションキーは、Lambda の環境変数に直書きせず Secrets Manager で管理します。
- [AWS Secrets Manager] → [新しいシークレットを保存する] を押下
- シークレットのタイプで [その他のシークレットのタイプ] を選択し、以下のキー / 値を登録
| キー | 値 |
|---|---|
pagerduty_routing_key |
PagerDuty サービスのインテグレーションキー |
3. シークレット名を設定して保存します(例: elasticache-event-notification)
④ Lambda 実行ロールの作成
- [IAM] → [ロール] → [ロールを作成] を押下
- 信頼されたエンティティ: [AWS のサービス] → [Lambda]
- 以下の権限を付与します
| ポリシー | 用途 |
|---|---|
AWSLambdaBasicExecutionRole(AWS 管理ポリシー) |
CloudWatch Logs への出力 |
| インラインポリシー(下記) | Secrets Manager からの認証情報取得 |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:<リージョン>:<AWSアカウントID>:secret:<シークレット名>-*"
}
]
}
⑤ Lambda 関数の作成
- [AWS Lambda] → [関数の作成] → [一から作成] を押下
- 以下を設定します
| 項目 | 値 | 備考 |
|---|---|---|
| 関数名 | 例) elasticache-event-notification |
— |
| ランタイム | Python 3.13 | — |
| アーキテクチャ | x86_64 | — |
| 実行ロール | ④で作成したロールを選択 | — |
| タイムアウト | 1 分 |
3. [設定] → [環境変数] に以下を設定します
| キー | 値 |
|---|---|
NOTIFICATION_SECRET_ID |
③で作成したシークレットの名前または ARN |
PD_SEVERITY |
critical(省略時は critical) |
4. 以下のコードをデプロイします
import json
import os
import urllib.error
import urllib.request
import boto3
TITLE_TEMPLATE = "[elasticache-redis-failover発生] 対象:{target}"
EVENT_KEY_PREFIX = "ElastiCache:"
SECRET_ID = os.environ["NOTIFICATION_SECRET_ID"]
PD_SEVERITY = os.environ.get("PD_SEVERITY", "critical")
PD_ENDPOINT = "https://events.pagerduty.com/v2/enqueue"
_secret_cache = None
def _get_secret():
global _secret_cache
if _secret_cache is None:
client = boto3.client("secretsmanager")
response = client.get_secret_value(SecretId=SECRET_ID)
_secret_cache = json.loads(response["SecretString"])
return _secret_cache
def _parse_message(raw_message):
try:
body = json.loads(raw_message)
except json.JSONDecodeError:
body = None
if not isinstance(body, dict) or not body:
return "ElastiCache:Event", "", {"Message": raw_message}
def _as_text(value):
return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False)
for key, value in body.items():
if key.startswith(EVENT_KEY_PREFIX):
return key, _as_text(value), body
event_name, value = next(iter(body.items()))
return event_name, _as_text(value), body
def _extract_target(event_value):
value = event_value.strip()
target = value.rsplit(":", 1)[-1].strip() if ":" in value else value
return target or "unknown"
def _region_from_arn(arn):
parts = arn.split(":")
return parts[3] if len(parts) > 3 else ""
def _build_title(target):
return TITLE_TEMPLATE.format(target=target)
def _post_json(url, payload, headers=None):
data = json.dumps(payload).encode("utf-8")
request_headers = {"Content-Type": "application/json"}
if headers:
request_headers.update(headers)
req = urllib.request.Request(url, data=data, headers=request_headers, method="POST")
with urllib.request.urlopen(req, timeout=10) as response:
return response.status, response.read().decode("utf-8")
def _notify_pagerduty(routing_key, title, target, event_name, body, sns):
payload = {
"routing_key": routing_key,
"event_action": "trigger",
"dedup_key": sns["MessageId"],
"payload": {
"summary": title,
"source": sns["TopicArn"],
"severity": PD_SEVERITY,
"timestamp": sns["Timestamp"],
"custom_details": {
"Service": "ElastiCache",
"EventName": event_name,
"Target": target,
"Region": _region_from_arn(sns["TopicArn"]),
"EventMessage": body,
"TopicArn": sns["TopicArn"],
"SnsMessageId": sns["MessageId"],
},
},
}
return _post_json(PD_ENDPOINT, payload)
def lambda_handler(event, context):
secret = _get_secret()
errors = []
for record in event["Records"]:
sns = record["Sns"]
event_name, event_value, body = _parse_message(sns["Message"])
target = _extract_target(event_value)
title = _build_title(target)
print(json.dumps({"event_name": event_name, "title": title}, ensure_ascii=False))
routing_key = secret.get("pagerduty_routing_key") or ""
if not routing_key:
print("pagerduty: skipped (credential is not set)")
continue
try:
status, response_body = _notify_pagerduty(
routing_key, title, target, event_name, body, sns
)
print(f"pagerduty: status={status} body={response_body}")
except urllib.error.HTTPError as exc:
errors.append(f"pagerduty: HTTP {exc.code} {exc.read().decode('utf-8')}")
except Exception as exc: # noqa: BLE001
errors.append(f"pagerduty: {exc!r}")
if errors:
raise RuntimeError("; ".join(errors))
return {"status": "ok"}
⑥ サブスクリプションとフィルターポリシーの設定
(1) Lambda サブスクリプションの作成
- [Amazon SNS] → [トピック] → ①のトピック → [サブスクリプションの作成] を押下
- 以下を設定します
| 項目 | 値 | 備考 |
|---|---|---|
| プロトコル | AWS Lambda | — |
| エンドポイント | ⑤で作成した Lambda 関数を選択 | — |
| サブスクリプションフィルターポリシー | 有効 | 下記 (2) 参照 |
| フィルターポリシーのスコープ | メッセージ本文 | 既定の「メッセージ属性」では動作しません |
(2) サブスクリプションフィルターポリシー
前述のとおり、ElastiCache はフェイルオーバー以外のイベント(作成 / 削除 / スケーリング / メンテナンス等)も同一トピックに発行します。フェイルオーバーイベントのみを通知対象とするため、以下のフィルターポリシーを設定します。
{
"ElastiCache:FailoverComplete": [
{
"exists": true
}
]
}

ElastiCache のメッセージ本文はイベント名がそのまま JSON のキーになるため、exists 演算子でキーの有無を判定するだけで絞り込めます。
なお、Lambda 側でイベント名を判定して弾く方法もありますが、今回は SNS のフィルターポリシーで絞ります。
⑦ 動作確認
ElastiCache に対してフェイルオーバーを発生させたところ、PagerDuty に想定どおりの内容でインシデントが起票されました。

まとめ
今回、ElastiCache のイベント通知を Amazon SNS で受け取り、Lambda 経由で PagerDuty にインシデントとして起票する手順を紹介しました。
最後までご覧いただきありがとうございました。