Article:

AWS Lambdaは非常に格安で便利なサービスですよね。僕ももう、個人のアプリケーションも仕事のお手伝いのものも、まず最初にLambdaで実装できないか検討しています。

ただ、特に個人のものになるとLambdaは作りっぱなしになりがちでした。作ってしばらく放置してて、気づいたらなんか動いてない… ってこともあったり。

そこで、Lambdaの実行が失敗したときや実行時間が長すぎたときなど、ある条件が発生した場合にDiscordに通知がくる仕組みを作ったのでその紹介をします。

こんな通知が飛んでくるようになります。

こんな感じの通知

この仕組みは、大きく分けて次の3つの部分から構成されます。

  • 1 Lambdaの実行を監視して、特定条件下でイベントを発火させる部分
  • 2 発火されたイベントを受け取り、lambdaを呼び出す部分
  • 3 discordのwebhookを叩くlambda

3番はlambdaとして、1はCloudWatchが, 2はSimple Notification Serviceが担当します。

CloudWatchはよくlambdaのログを見るのにお世話になっているとは思いますが、ログの他にも今回使う「アラーム」の機能、またちょっと意外なのですがawsに起こるイベントを監視して他のサービスを発火させたり、lambdaなどのの定期実行のスケジューリングなんかも可能です。

今回はCloudWatchのアラーム機能を使って、Lambdaのエラーを検出します。

Simple Notification Service(SNS)はその名の通り通知を司るサービスなのですが、awsで発生する通知のほとんどはこのSNSをかませる事ができて、その出力をLambda, HTTP, SMS, Eメールなどに振り分けることができます。

今回はこのサービスを使ってCloudWatchのアラームをLambdaに転送します。


AWSでの設定の前に、まずDiscordの通知を受け取りたいサーバーでwebhookを作成しておきます。

作成は簡単で、サーバー設定>連携サービスから、「新しいウェブフック」を選択して新しいウェブフックを作成します。Botの名前、アイコン、通知を行うチャンネルを設定したら、「ウェブフックURLをコピー」を押してURLを取得します。これは後で使います。


ではAWSの方の作業を始めていきましょう。

まずはSNSを使います。

今回SNSのキーワードとなるのは「トピック」と「サブスクリプション」の2つです。

名前からなんとなく想像ができるとは思いますが、「トピック」がいわゆるイベントの受信箱です。そして「サブスクリプション」がトピックで受信したイベントをどこに配送するかの設定になります。

まずはトピックを作成します。

SNSを開いてトピックの作成を選択します。

タイプはスタンダードを選択します。

トピックの作成

詳細以外の項目の設定は特に必要ありません。そのまま「トピックの作成」をします。

サブスクリプションはLambdaを設定するときに自動生成されるのでもうSNSは閉じてしまって、次にCloudWatchの設定をします。


CloudWatchを開き、「アラームの作成」を選択します。

で、ここで「メトリクスの選択」をすることになります。

メトリクスとはざっくり言うとAWSの各種リソースの状態を数値化したものです。

今回はLambda関数のエラーレートを監視したいので、まず「すべてのメトリクス」タブからLambda→関数名別を選択します。

すると、関数名とメトリクス名の組が出てくるので、ここで監視したい関数名の、メトリクス名「エラー(Errors)」になっているカラムにチェックを入れて「メトリクスの選択」をクリックします(別にエラー以外の項目でも、自分の監視したい項目を選択して頂いてOKです。)。

つぎにアラームを出す条件を設定します。

自分がちょろ~っと使ってる奴であれば1個エラーが出たら一応通知してほしいので、次のように設定しました。(このような状態になることが想定されるようなサービスでは慎重に判断してください!)

条件

次に通知の設定をします。

通知

アラーム状態トリガーを「アラーム状態」に、SNSトピックの選択を「既存のSNSトピックを選択」にして、「通知の送信先」で先程作成したトピックを選択します。

(ここで「新しいトピックの作成」をしても良かったのですが、なぜかメールアドレスの入力が必須になっており、メール配送のサブスクリプションが強制的に生成されてしまいます。ここではDiscordの通知しかいらないのでこのような手順を踏みました。)

これだけ設定したら次へ進んでOKです。

次に名前と説明を入力します。適当でOKです。

最後に今まで設定した項目の一覧が出てくるので、確認したら「アラームの作成」を押してアラームを作成します。

アラームを作成し、そのアラームを受け取るトピックも作成したので、最後にこのアラームをDiscordに配送するLambdaを作成します。


Lambdaで「関数を作成」を押してウィザードを開きます。

SNSをハンドルする便利なテンプレートが予め用意されているので、「設計図の使用」を選択して、検索窓に「sns」と入力すれば、「設計図名: sns-message-python」が見つかると思うのでそれを利用することにします。

関数名を適当に入力して、実行ロールも特になければそのままにします。

つぎにSNSトリガーの設定項目があるので、そこで先程作成したSNSトピックを選択します。

そしたらもう「関数の作成」を押して大丈夫です。

で、罠なんですがこのテンプレートめっちゃ古くて、なんとランタイム設定がPython2.7になってます。ランタイム設定からPython3.7に設定を上げておきます。

アラートが発生するとeventに次のようなオブジェクトが投げ込まれます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
   "Records":[
      {
         "EventSource":"aws:sns",
         "EventVersion":"1.0",
         "EventSubscriptionArn":"arn:aws:sns:ap-northeast-1:<省略>",
         "Sns":{
            "Type":"Notification",
            "MessageId":"<省略 なんかID>",
            "TopicArn":"arn:aws:sns:ap-northeast-1:<省略>",
            "Subject":"ALARM: \"compile error\" in Asia Pacific (Tokyo)",
            "Message":"{<省略 (次に詳細を記載)>}",
            "Timestamp":"2021-02-15T15:53:14.501Z",
            "SignatureVersion":"1",
            "Signature":"<省略 なんか証明>",
            "SigningCertUrl":"<省略 なんかpem>",
            "UnsubscribeUrl":"https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe<省略>",
            "MessageAttributes":{
               
            }
         }
      }
   ]
}

これがSNSの枠組みです。一番重要なのはRecords→Sns→Messageです。SNSではここにサービスごとの自由なメッセージが入っていて、CloudWatchのアラート情報もここにJsonが文字列化された状態で書き込まれます。

messageをデコードすると次のオブジェクトが得られます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
    "AlarmName": "アラーム名",
    "AlarmDescription": "アラーム詳細",
    "AWSAccountId": "アカウントID",
    "NewStateValue": "ALARM",
    "NewStateReason": "Threshold Crossed: 1 out of the last 1 datapoints [1.0 (15/02/21 14:52:00)] was greater than or equal to the threshold (1.0) (minimum 1 datapoint for OK -> ALARM transition).",
    "StateChangeTime": "2021-02-15T14:57:14.419+0000",
    "Region": "Asia Pacific (Tokyo)",
    "AlarmArn": "アラームのARN",
    "OldStateValue": "INSUFFICIENT_DATA",
    "Trigger": {
        "MetricName": "Errors",
        "Namespace": "AWS/Lambda",
        "StatisticType": "Statistic",
        "Statistic": "MAXIMUM",
        "Unit": null,
        "Dimensions": [
            {
                "value": "Lambda関数名",
                "name": "FunctionName"
            }
        ],
        "Period": 300,
        "EvaluationPeriods": 1,
        "ComparisonOperator": "GreaterThanOrEqualToThreshold",
        "Threshold": 1,
        "TreatMissingData": "- TreatMissingData:                    ignore",
        "EvaluateLowSampleCountPercentile": ""
    }
}

だいたい必要なことが全部書いてあるのがわかるかと思います。

日曜大工のノリのコードなので雑ですが、いい感じにDiscordに送信してくれるコードはこんな感じになりました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from __future__ import print_function
import urllib.request
import json

print('Loading function')

# === :: 設定項目 :: ===========================
endpoint = "https://あなたのdiscordのwebhookURL"
accountName = 'AWSの組織名'
accountImage = 'なんかアイコンのURL'
# ==============================================

def post(url, data, headers ={}):
    req = urllib.request.Request(url,
                                 data=json.dumps(data).encode('utf-8'),
                                 headers=headers,
                                 method='POST')
    return urllib.request.urlopen(req)

def lambda_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])
    fields = [
        {'name': 'メトリック名', 'value': message['Trigger']['MetricName'], 'inline': True},
        {'name': '条件', 'value': message['Trigger']['ComparisonOperator'], 'inline': True}
    ]
    fields.extend([{'name': elem['name'], 'value': elem['value']} for elem in message['Trigger']['Dimensions']])
    data = {
        'embeds': [
            {
                'title': 'アラーム発生: ' + message['AlarmName'],
                'description': message['AlarmDescription'],
                'color': 0xFF0000,
                'timestamp': message['StateChangeTime'],
                'fields': fields,
                'author': {
                    'name': accountName,
                    'icon_url': accountImage
                }
            }
        ]
    }
    
    post(endpoint, data=data, headers={'Content-Type': 'application/json', "User-Agent": "curl/7.64.1"})
    
    return {
        'statusCode': 200
    }

めんどくさいので外部ライブラリを入れたくなかったのでurllibを使ってhttp通信しています。

なんかDiscordちゃんはurllibは弾くらしいのでUserAgentを偽装しています。これは@PharaohKJさんの記事を参考にしました。助かりました。ありがとうございます。

Discord上でのリッチビューの作り方は@Eaiさんの記事を参考にしました。ありがとうございます。

Lambda上でのテストイベントのテンプレートにはCloudWatch Alarmを扱うものがなかったので手頃なものを用意しました。これを使ってみてください。

うまく行ったらこんな感じの通知が飛んでくるはずです。

テスト結果

テストしてうまく行ったらそれでもう動きそうな気がしますが、なんと、作成直後のLambda関数は紐付けたSNSのトリガーが無効化されています。まぁSNSには通知が濁流のように流れていることもあるので、作成した動かない関数がめっちゃ呼ばれて死亡みたいなことを防ぐためですね。

なのでテストが成功したら有効化を忘れずに。有効化は、Lambda関数の編集画面の一番上にある「デザイナー」ブロックでトリガーの部分にSNSのアイコンがあると思うのでそれをクリック、下に表示されているトリガーを選択して、「有効化」ボタンです。

これで完成です!


AWSの様々なサービスはCloudWatchで観測可能なメトリクスを持っているので、Lambda以外のサービスの監視ももちろん今回の方法が応用して使えます。

また、CloudWatch以外のサービス、例えばこのブログのHUGOで個人サイトを作成・ホストするで使っているAWS AmplifyなんかもSNSへイベントを送信してメール配送を行っているので、今回作成したLambdaをちょっとだけ変えたLambdaを作成することで、ビルド通知をメールではなくDiscordで受け取るようにすることができます。もちろんDiscord以外のSlack、Telegramでも使えますね。

AWSはかなりモジュラリティが高く構成されており、またLambdaのテンプレートも豊富にあることからこんな記事がなくても多分ほとんどの人はポチポチーっと作れちゃうとは思いますが、「このようなことができる」ってことを伝えるためにこの記事を書きました。少しでも参考になれば幸いです。

P.S.

AWS Amplifyのビルド通知を飛ばすLambdaのコードを書いたので僕のGistで共有しておきます。よかったらぜひ参考にしてみてください。

こんな感じの通知が出ます。

Amplify通知サンプル

Outline: