
AWS DevOps Agent (Preview)をPagerDutyからトリガーしてみた #AWSreInvent
こんにちは。たかやまです。
以前、PagerDuty連携とWebhook設定を使ってDevOps Agentを起動する検証を行いました。
この時は、PagerDutyから直接DevOps Agentを起動する方法を試そうと思いましたがネイティブの設定がなさそうだったので断念しました。
今回はそのリベンジとして、PagerDutyのIncident Workflowsを使ってDevOps Agentをトリガーする方法を検証しました。
やってみる
今回構築する構成は以下のようになります。

シンプルにCloudWatch AlarmからDevOps Agentを起動する方法は以下のブログを参照してください。
PagerDutyのインテグレーション設定
CloudWatch AlarmとPagerDutyを連携する方法には、「Event Orchestration」と「PagerDuty Service」を使う2つのパターンがあります。
今回はPagerDuty Serviceを作成して、CloudWatch Alarmとの連携を行います。

Amazon CloudWatch Integration Guide | PagerDuty
PagerDutyのServicesメニューからNew Serviceボタンをクリックしてサービスを作成します。

サービス作成ウィザードが表示されます。ステップ1 Name and Description 〜 ステップ3の Reduce Noise までは適宜設定していきます。



ステップ4のIntegrationsでは、アラートを送信する連携先を選択します。Amazon CloudWatchを検索して選択します。

サービスが作成されると、Amazon CloudWatch Integrationの設定画面が表示されます。
この後のSNSトピックの設定で利用するのでIntegration URLを控えておきます。

PagerDuty インテグレーション用のSNSトピックを作成
AWSマネジメントコンソールからAmazon SNSのトピックを作成します。トピックタイプはスタンダードを選択し、トピック名を入力します。

作成したトピックにサブスクリプションを設定していきます。
プロトコルはHTTPSを選択し、エンドポイントには先ほど控えたPagerDutyのIntegration URLを入力します。
PagerDutyとの連携において、rawメッセージ配信は無効にしておきます。
- プロトコル : HTTPS
- エンドポイント : PagerDutyのIntegration URL
- rawメッセージ配信 : 無効

CloudWatch AlarmとSNSトピックの連携
CloudWatch Alarmの設定を行います。今回は以前作成したテストオプションBの環境(Lambda Error Testアラーム)を利用します。
CloudWatch Alarmの詳細画面で、アラームの状態を確認できます。この例ではOK状態になっています。

アラームの編集画面に移動し、アクションの設定を行います。PagerDutyにインシデントを通知するためには、アラーム状態とOK状態の両方でSNSトピックを設定する必要があります。
- アラーム状態トリガー : 作成したSNSトピックを選択
- OK状態トリガー : 作成したSNSトピックを選択

設定のプレビュー画面で、メトリクス、条件、アクションが正しく設定されていることを確認します。

Lambda関数を作成する
PagerDutyからDevOps AgentのWebhookをトリガーするLambda関数を作成します。
Webhookの作成については以下のブログを参照してください。
Lambda関数の内容は以下のとおりです。
この後設定するPagerDutyのIncident Workflowからインシデント情報を受け取り、DevOps AgentのWebhookに通知しています
// index.mjs
import { createHmac } from "node:crypto";
import https from "node:https";
import { URL } from "node:url";
const EVENT_AI_WEBHOOK_URL = process.env.EVENT_AI_WEBHOOK_URL;
const EVENT_AI_SECRET = process.env.EVENT_AI_SECRET;
const AWS_ACCOUNT_ID = process.env.AWS_ACCOUNT_ID || "unknown";
const AWS_REGION = process.env.AWS_REGION || "us-east-1";
export const handler = async (event) => {
try {
console.log("Raw event:", JSON.stringify(event, null, 2));
if (event.source !== "pagerduty") {
console.warn("Not a PagerDuty event, skipping:", event.source);
return { statusCode: 200, body: "Not a PagerDuty event, ignored" };
}
const payload = processPagerDutyEvent(event);
const response = await sendToEventAi(payload);
console.log("event-ai response:", response.statusCode, response.body);
return response;
} catch (err) {
console.error("Error in event handler:", err);
return { statusCode: 500, body: "Internal server error" };
}
};
function processPagerDutyEvent(event) {
console.log("Processing PagerDuty event");
const incidentId = event.incident_id || "";
const incidentTitle = event.incident_title || "PagerDuty Incident";
const incidentStatus = event.incident_status || "";
const incidentUrl = event.incident_url || "";
// alert_detailsからCloudWatch Alarm情報を取得
const alertDetails = event.alert_details?.body?.details || {};
const cefDetails = event.alert_details?.body?.cef_details || {};
const trigger = alertDetails.Trigger || {};
const dimensions = trigger.Dimensions || [];
// timestampは現在時刻を使用(重複回避のため)
const timestamp = new Date().toISOString();
// descriptionを構築(alert_detailsから詳細情報を取得)
const descriptionLines = [];
descriptionLines.push("Please respond in Japanese.");
descriptionLines.push(`Status: ${incidentStatus}`);
if (alertDetails.NewStateReason) {
descriptionLines.push(`Reason: ${alertDetails.NewStateReason}`);
}
// Lambda関数名をDimensionsから取得
const functionDim = dimensions.find(d => d.name === "FunctionName");
if (functionDim?.value) {
descriptionLines.push(`Lambda Function: ${functionDim.value}`);
}
if (trigger.Namespace && trigger.MetricName) {
descriptionLines.push(`Metric: ${trigger.Namespace}/${trigger.MetricName}`);
}
if (alertDetails.AlarmName) {
descriptionLines.push(`Alarm Name: ${alertDetails.AlarmName}`);
}
if (alertDetails.Region) {
descriptionLines.push(`Region: ${alertDetails.Region}`);
}
if (alertDetails.AWSAccountId) {
descriptionLines.push(`Account ID: ${alertDetails.AWSAccountId}`);
}
if (alertDetails.StateChangeTime) {
descriptionLines.push(`Incident Time: ${alertDetails.StateChangeTime}`);
}
if (incidentUrl) {
descriptionLines.push(`URL: ${incidentUrl}`);
}
const description = descriptionLines.join("\n");
// affectedResources: alert_detailsからARNを取得
const affectedResources = [];
if (alertDetails.AlarmArn) {
affectedResources.push(alertDetails.AlarmArn);
}
if (cefDetails.source_origin) {
affectedResources.push(cefDetails.source_origin);
}
// alert_detailsにARNがない場合はダミーARNを生成
if (affectedResources.length === 0) {
affectedResources.push(`arn:aws:pagerduty:${AWS_REGION}:${AWS_ACCOUNT_ID}:incident/${incidentId}`);
}
return {
eventType: "incident",
incidentId: `pagerduty:${incidentId}`,
action: "created",
priority: "HIGH",
title: incidentTitle,
description,
timestamp,
service: "pagerduty",
affectedResources
};
}
async function sendToEventAi(payload) {
const payloadString = JSON.stringify(payload);
const tsHeader = new Date().toISOString();
const hmac = createHmac("sha256", EVENT_AI_SECRET);
hmac.update(`${tsHeader}:${payloadString}`, "utf8");
const signature = hmac.digest("base64");
const url = new URL(EVENT_AI_WEBHOOK_URL);
const options = {
hostname: url.hostname,
path: url.pathname + url.search,
method: "POST",
port: url.port || 443,
headers: {
"Content-Type": "application/json",
"x-amzn-event-timestamp": tsHeader,
"x-amzn-event-signature": signature,
"Content-Length": Buffer.byteLength(payloadString)
}
};
console.log("Sending payload to event-ai:", JSON.stringify(payload, null, 2));
console.log("Request headers - timestamp:", tsHeader, "signature:", signature);
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk;
});
res.on("end", () => {
resolve({
statusCode: res.statusCode || 200,
body: data
});
});
});
req.on("error", (err) => reject(err));
req.write(payloadString);
req.end();
});
}
主にやっていることは以下のとおりです。
ペイロード作成部分では同僚の林から日本語指示を入れるとDevOps Agentが日本語で回答してくれると聞いたので日本語指示も追加しています。
- PagerDutyイベントの受信と検証
- PagerDuty Workflowから送信されたイベントからインシデントIDやタイトル、ステータスなどの基本情報を取得
- CloudWatch Alarm情報の抽出
alert_detailsからCloudWatch Alarmの詳細情報(アラーム名、メトリクス、リージョン、Lambda関数名など)を取得
- DevOps Agent用ペイロードの構築
- インシデント情報とCloudWatch Alarmの詳細を組み合わせて、DevOps Agentが理解できる形式に変換
- (小ネタ)日本語で対応するための指示を追加
- event-ai Webhookへの送信
- HMAC-SHA256署名を使用した認証
- タイムスタンプと署名をヘッダーに含めてHTTPS POSTリクエストを送信
以下の環境変数にはWebhook作成で取得した値を設定します。
EVENT_AI_WEBHOOK_URL: DevOps AgentのWebhook URLEVENT_AI_SECRET: Webhook認証用のシークレット

PagerDutyでIncident Workflowsを作成する
PagerDutyのAutomation > Incident Workflowsを選択し、New Workflowボタンをクリックします。

ワークフロー作成ダイアログが表示されるので、ワークフロー名にInvoke DevOps Agentを入力し、Createを選択します。

ワークフローが作成されると、ビルダー画面が表示されます。
この画面でトリガーとアクションを設定していきます。

ワークフローのトリガー条件では、Conditional Triggerを設定します。この設定でワークフローを起動する条件を定義します。
今回は以下の内容を指定します。
- When should this workflow start?: "When an incident is created"(インシデントが作成されたとき)
- This trigger applies to: "Specific services"(特定のサービスに対して適用)
- Services that this rule applies to: 最初に作成した PagerDuty Service (CloudWatch Alarmと連携したサービス) を選択
これにより、CloudWatch Alarmから送信されたインシデントが作成された際に、自動的にこのワークフローが起動されます。

ワークフローのアクションでは、以下の2つのステップを設定します。
1つ目のアクションとしてGet Alerts for an Incidentを追加します。
このアクションはインシデントに関連するアラート情報を取得するもので、Incident IDには{{incident.id}}を指定します。

2つ目のアクションとしてAWS: Invoke a Lambda Functionを追加します。このアクションでDevOps Agentを起動するLambda関数を呼び出します。
AWS: Invoke a Lambda Functionの設定では以下を指定します。
- Integration: 事前に設定したAWS接続を選択
- Function name:
cloudwatch-alarm-to-devops-agent(DevOps Agent起動用のLambda関数名) - Payload: JSON形式でインシデント情報を含むペイロードを設定
- Invocation type:
RequestResponse(同期呼び出し) - Region:
us-east-1(Lambda関数がデプロイされているリージョン)
Payloadには、インシデント情報とアラート詳細を含むJSON構造を指定します。
{
"source": "pagerduty",
"incident_id": "{{incident.id}}",
"incident_title": "{{incident.title}}",
"incident_status": "{{incident.status}}",
"incident_url": "{{incident.url}}",
"created_at": "{{incident.created_at}}",
"alert_details": {{steps['Get Alerts for an Incident'].fields['First Alert Details']}}
}

ここで重要な注意点があります。alert_detailsに渡している{{steps['Get Alerts for an Incident'].fields['First Alert Details']}}は、前段に設定したアクションGet Alerts for an Incidentのフィールドを渡すものです。
他のフィールドと異なり、この値はすでにJSON形式のオブジェクトであるため、ダブルクォートで囲まずにネストされたオブジェクトとして扱う必要があります。
ダブルクォートで囲んでしまうと、文字列として扱われてしまい、Lambda関数側でJSONとしてパースできなくなり以下のエラーが発生しました。
Amazon.Runtime.Internal.HttpErrorResponseException
以下がCloudWatchから送信される実際のイベント構造です。この構造全体がalert_detailsとして渡されます。

インシデントを起こしてDevOps Agentの調査を確認する
では、実際にテストオプションBのLambda関数を起動して、DevOps Agentが調査を開始することを確認します。
テストオプションBで作成された AWS-AIDevOps-test-lambdaのテストを実行してアラームを起動します。

CloudWatch Alarmが起動してPagerDutyにインシデントが作成されます。


PagerDutyのTimelineからIncident Workflowが起動していることを確認します。
ちゃんとWorkflowが起動していますね

DevOps AgentもLambda関数で作成したDescriptionに従って調査を開始しています。

また、小ネタで仕込んだ日本語の指示に従ってちゃんと回答してくれています。

最後に
PagerDutyからDevOps Agentをトリガーする方法を検証しました。
以前の検証ではPagerDutyのネイティブ連携でDevOps Agentを起動する方法は見当たりませんでしたが、Incident Workflowsを経由することで実現できました。
PagerDutyを利用することで、AIOpsの恩恵や柔軟なインシデントトリガーによるDevOps Agentの起動が可能になると思います。
今後、インシデント対応の自動化や効率化を検討される際の一助となれば幸いです。
以上、たかやま(@nyan_kotaroo)でした。









