4
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Serverless Framework でエラーを検知して Webhook で Slack に通知を飛ばす方法

はじめに

AppSync の Lambda リゾルバを書く際に Serverless Framework を使用したのですが、
デプロイ後のバグ調査の際、毎回ブラウザから AWS Console を開いて該当 Lambda の CloudWatch のログを見に行くのが面倒でした。。 :upside_down:

そのため、エラーレポートの仕組みが欲しくなり、Lambda のエラーを Slack に通知する仕組みを Serverless Framework で実装する方法について調査したので、備忘録も兼ねて記事にまとめました :writing_hand:

追記 (2020/08/19)

ローカルから Lambda 関数のログを確認したいだけなら、Serverless Framework CLI の logs コマンド もしくは、 AWS CLI の logs コマンド で可能みたいです :mag:

動作環境

1. Slack で Webhook URL を発行する

まずは 公式サイトの手順 に従って Webhook URL を発行します :earth_americas:

無事発行できると、
https://hooks.slack.com/services/~~~~~/~~~~~/~~~~~~~~~~~ のようなフォーマットの URL が取得出来るはずなのでメモっておきます :pencil:

2. 必要な npm パッケージをインストールする

Slack の Incoming Webhook の仕組みを使用し、
チャンネルにメッセージを送信するための npm パッケージをインストールします :arrow_down:

npm install @slack/webhook --save

3. Slack にエラーレポートを送信する Lambda 関数を作成する (TypeScript)

Serverless Framework の handler に Slack にエラーレポートを送信する関数を追加します :arrow_down:

handlers/Reporter.ts
import { gunzip } from "zlib";
import { IncomingWebhook } from "@slack/webhook";

/**
 * CloudWatch のログ情報
 */
interface CloudWatchLogContent {
  messageType: string;
  owner: string;
  logGroup: string;
  logStream: string;
  subscriptionFilters: string[];
  logEvents: {
    id: string;
    timestamp: string;
    message: string;
  }[];
}

/**
 * Lambda リゾルバの型定義
 */
type LambdaResolver<TEvent = any> = (event: TEvent) => Promise<any> | any;

/**
 * CloudWatch のロググループに出現するエラーを通知する
 * @param event 該当するエラーログの内容
 * @return {object} event オブジェクトをそのまま返却する
 */
export const NotifyError: LambdaResolver = async (event: any) => {
/**
{
  awslogs: {
    data: 'H4sIAAA...'
  }
}
CloudWatch から呼ばれた際の event には上記フォーマットでデータが入っている。
data 内には Base64 でエンコードされた gzip 形式で圧縮されたデータが入っているので、
gzip 形式のデータを解凍しつつ、Base64 デコードを行い JSON 文字列を取得するための関数
*/
  const gunzipAsync = async (base64Logs): Promise<string> => {
    return new Promise(function (resolve, reject) {
      gunzip(base64Logs, function (err, binary) {
        err ? reject(err) : resolve(binary.toString("ascii"));
      });
    });
  };

// 1. Base64 でエンコードされた gzip 形式で圧縮されたデータを Base64 でデコードし、gzip のバイナリとして取得する
// gunzipAsync 関数で gzip 解凍して ascii 文字列として取得することで CloudWatch のログ内容を JSON 文字列で取得する
  const base64Logs = Buffer.from(event["awslogs"]["data"], "base64");
  const uncompressedLogs = await gunzipAsync(base64Logs);
  console.log(uncompressedLogs);

// 2. 取得した JSON 文字列を CloudWatchLogContent に変換して取得する
  const content = <CloudWatchLogContent>JSON.parse(uncompressedLogs);
  console.log(content);

// 3. 発行した Slack の Webhook URL で IncomingWebhook クラスを生成し、
// send 関数で Slack チャンネル名 (ex. #serverless-error-report) と、
// CloudWatchLogContent の内容を元に作成したテキストを引数に指定して、
// 該当する Slack チャンネルにテキストを投稿する
  const webhook = new IncomingWebhook("<1. で発行した Slack の Incoming Webhook URL>");
  await webhook.send({
    channel: "<通知したいチャンネル名 (例: #serverless-error-report)>",
    icon_emoji: "hammer", // icon_emoji パラメタを指定すると Slack へのメッセージ通知の際のアイコンを変更することが可能
    text: `*Group*\n_${content.logGroup}_\n\n*Message*\n\`\`\`${content.logEvents[0].message}\`\`\``,
  });
  return event;
};

4. 3. の関数をデプロイする関数として追加し、各種 Lambda 関数の CloudWatch のログを監視するイベントと紐付ける

serverless.yml手順 3. で作成した関数 NotifyError を記載すると共に、
監視したい関数の CloudWatch ロググループを eventscloudwatchLog に定義し、filterERROR を指定します :heavy_check_mark:

これで、
該当ロググループに ERROR が含まれていた場合、
都度 Lambda 関数が実行されるようになります :fire: :arrow_down:

serverless.yml
#...
# 3. で作成した Slack にエラーレポートを送信する関数 NotifyError を functions に追記し、
# events を用いて、他 Lambda 関数のロググループに 'ERROR' が出力されていた場合、 NotifyError 関数が実行されるようにする
functions:
#...
  NotifyError:
    handler: CloudWatch.NotifyError
    events:
      - cloudwatchLog:
          logGroup: /aws/lambda/${self:service.name}-${self:provider.stage}-TestFunction1
          filter: ERROR
      - cloudwatchLog:
          logGroup: /aws/lambda/${self:service.name}-${self:provider.stage}-TestFunction2
          filter: ERROR
      - cloudwatchLog:
          logGroup: /aws/lambda/${self:service.name}-${self:provider.stage}-TestFunction3
          filter: ERROR
#...

:arrow_up: が完了したら sls deployNotifyError 関数をデプロイします :keyboard:

最後に AWS CLI で CloudWatch にイベントログデータを送信してみて、
本当に Slack に通知が飛んでくるか動作確認を行いましょう :hammer_pick:

注意事項 (複数の cloudwatchLog を定義した場合)

events に複数の cloudwatchLog を定義した場合、
関数のデプロイ後、AWS Console で該当する Lambda 関数を見に行くと、
正しく CloudWatch のイベントが紐付けられていないように見えます :arrow_down:
492d4761e7fadb62612e539c17b3d05f.png

同様のケースが発生している方は他にもいらっしゃるようですが、
手順 5. で Slack への通知まで確認出来れば問題なく設定できています :thumbsup:

5. AWS CLI を利用してCloudWatch にイベントログデータを送信して Slack に通知が飛んでくるか検証する

CloudWatch にイベントログデータを送信するコマンドです :white_check_mark: :arrow_down:

aws logs put-log-events \
    --log-group-name '<該当するロググループ名>(例: /aws/lambda/test-dev-TestFunction1)' \
    --log-stream-name '<ログストリーム名(例: test-stream)>' --log-events \
    timestamp=(node -e 'console.log(Date.now())'),message="This is ERROR"

コマンド実行後、
AWS Console から CloudWatch の該当するロググループのログストリームを見に行くと、
This is ERROR という文字列が出力されている事が確認できるはずです :mag:

あとは Slack に通知が飛んできたことまで確認できれば動作確認完了です! :tada: :arrow_down:
4a5d063485ed635c83e3ea1917043673.png

おわりに

Serverless Framework 内で完結する形で、
Lambda 関数のエラーを捕捉して Slack に通知を飛ばす方法についてまとめました :writing_hand:

events には cloudwatchLog の他にも eventBridge というものも指定できます。
eventBridge を使用すると CloudWatch 以外の様々な AWS サービスのイベント駆動で Lambda 関数を実行することが可能です :muscle:

events を有効活用することで効率よくイベント駆動の処理を書いていけるので、
是非とも有効活用していきましょう! :runner: :dash:

参考リンク

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
4
Help us understand the problem. What are the problem?