はじめに

CloudWatchで取得しているCloudFrontのメトリクス情報を通知するLINE BOTを作ってみました。

CloudFrontへのリクエスト数や、エラー率を簡単に確認したいなと思い、Lambdaの勉強を兼ねて今回はLINE Messaging APIと連携してみました。

通知されるデータ

今回作ったLINE BOTから送られるデータはこちら。 result

通知するメトリクスデータは以下としました。

メトリクス名 統計値 期間 メトリクス内容
Requests Sum 1時間 すべての HTTP メソッド、および HTTP リクエストと HTTPS リクエストの両方について CloudFront が受信したビューワーリクエストの総数。
4xxErrorRate Average 1時間 レスポンスの HTTP ステータスコードが 4xx であるすべてのビューワーリクエストの割合 (%)。
5xxErrorRate Average 1時間 レスポンスの HTTP ステータスコードが 5xx であるすべてのビューワーリクエストの割合 (%)。

[参考] https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/programming-cloudwatch-metrics.html

構成図

今回作成した仕組みの構成図と処理の流れは以下の通りです。 archtecture

LambdaランタイムはPython 3.12を利用しています。

また、AWSの各サービスはバージニア北部(us-east-1)で作成しています。

理由として、CloudFrontのメトリクスはバージニア北部のリージョンに集約されるため、Lambda等もバージニア北部でないとデータが取得できないからです。

(Lambda関数を東京リージョンで作成してしまい、データが取得できず結構な時間ハマってしまいました…)

参考 https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/monitoring-using-cloudwatch.html

Line Messaging APIキーを取得

Line Messaging APIを利用するためには、LINE Developersコンソールからチャネルの作成を行う必要があります。

  • 公式サイトの以下の手順より、LINE Developersコンソールからチャネルを作成する https://developers.line.biz/ja/docs/messaging-api/getting-started/

  • チャネルを作成したら、チャネルアクセストークンを発行し、控えておく create_token token

  • 作成したチャネルのチャネル基本設定欄から、「あなたのユーザID」を控えておく userid

  • 作成したチャネルのMessaging API設定タブのQRコードを自分のLineに読み込ませておく

チャネルアクセストークンとユーザIDは後ほどSystems Manager Parameter Storeに登録します。

Systems Manager Parameter StoreにLambda関数で利用する変数を登録する

Lambda関数で先程取得したチャネルアクセストークンとユーザIDを利用します。

ただし、Pythonコード内に直接記載するのはコード流出時のセキュリティリスクや、万が一トークンが変更した際のコードメンテナンスの手間が発生します。

そのため、今回はSystems ManangerのParameter Storeに登録して、Lambda関数から呼び出す方式としました。

  • AWSマネジメントコンソールでバージニア北部リージョンでSystems Managerを開き、パラメータストア > パラメータの作成をクリック click_parameter_store

  • 以下の通り各項目を入力し、パラメータを作成をクリック

    • 名前:LINE_ACCESS_TOKEN
    • 利用枠:標準
    • タイプ:安全な文字列
    • KMS キーソース:現在のアカウント
    • KMS キー ID:alias/aws/ssm
    • 値:発行したチャネルアクセストークン set_parameter_1 set_parameter_2
  • パラメータの一覧に戻るので、LINE_ACCESS_TOKENが作成されていることを確認する finish_set_parameter

  • 再度パラメータの作成をクリックし、同じ要領で2つパラメータを登録する

  • 追加パラメータ1

    • 名前:LINE_USER_ID
    • 利用枠:標準
    • タイプ:安全な文字列
    • KMS キーソース:現在のアカウント
    • KMS キー ID:alias/aws/ssm
    • 値:LINE Developersコンソールのチャネル基本設定に記載されているユーザID
  • 追加パラメータ2

    • 名前:DISTRIBUTION_ID
    • 利用枠:標準
    • タイプ:安全な文字列
    • KMS キーソース:現在のアカウント
    • KMS キー ID:alias/aws/ssm
    • 値:LINEに通知させたいCloudFrontディストリビューションのID
  • 最終的に以下3つのパラメータが登録できていればOK parameter_list

Lambda用のIAMロールを作成する

Lambdaに付与するIAMロールとIAMポリシーを作成します。

IAMポリシーの作成

  • AWSマネジメントコンソールよりIAM > ポリシー > ポリシーを作成をクリック

  • ポリシーエディタでJSONをクリックし、以下のポリシーを入力後、次へをクリック

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "GetCloudWatchData",
      "Effect": "Allow",
      "Action": ["cloudwatch:GetMetricStatistics", "cloudwatch:ListMetrics"],
      "Resource": "*"
    },
    {
      "Sid": "GetSSMParameter",
      "Effect": "Allow",
      "Action": ["kms:Decrypt", "ssm:GetParameter"],
      "Resource": [
        "arn:aws:kms:ap-northeast-1:アカウントID:key/alias/aws/ssm",
        "arn:aws:ssm:us-east-1:アカウントID:parameter/*"
      ]
    }
  ]
}

【備考】Lambdaの実行ログをCloudWatch Logsに送信する場合は以下のようなポリシーも必要となるが、今回は無料枠の節約のため最終的に割愛した

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "logs:CreateLogGroup",
            "Resource": "arn:aws:logs:us-east-1:アカウントID:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "arn:aws:logs:us-east-1:アカウントID:log-group:/aws/lambda/関数名:*"
            ]
        }
    ]
}
  • 適当なポリシー名(今回はcloudwatch-lambda-test-policyとした)を入力し、ポリシーの作成をクリック policy_name

IAMロールの作成

作成したポリシーを紐付けたLambda用のIAMロールを作成します。

  • AWSマネジメントコンソールよりIAM > ロール > ロールを作成をクリック

  • 信頼されたエンティティタイプで「AWSのサービス」を選択し、ユースケースでLambdaを選択後、次へをクリック usecase

  • 先程作成したポリシーにチェックを入れ、次へをクリック attach_policy

  • 適当なロール名(今回はcloudwatch-lambda-test-roleとした)を入力し、ロールの作成をクリック role_name

ここで作成したIAMロールをLambdaに紐付けます。

Lambda関数とレイヤーを作成する

今回登録する関数の内容は以下になります。requestsライブラリを使用するため、Lambdaレイヤーも登録します。

import boto3
import json
import requests
from datetime import datetime, timedelta, timezone

# SSMクライアントの作成
ssm = boto3.client('ssm')
cloudwatch = boto3.client('cloudwatch', region_name='us-east-1')

# SSM Parameter StoreからLINEアクセストークンとユーザーIDを取得
LINE_ACCESS_TOKEN = ssm.get_parameter(Name='LINE_ACCESS_TOKEN', WithDecryption=True)['Parameter']['Value']
LINE_USER_ID = ssm.get_parameter(Name='LINE_USER_ID', WithDecryption=True)['Parameter']['Value']

# SSM Parameter StoreからCloudFrontディストリビューションIDを取得
DISTRIBUTION_ID = ssm.get_parameter(Name='DISTRIBUTION_ID', WithDecryption=True)['Parameter']['Value']

def lambda_handler(event, context):
    # 現在の時間と24時間前の時間を取得
    end_time = datetime.now(timezone.utc)
    start_time = end_time - timedelta(days=1)

    # 取得したいメトリクスの名前をリストで指定
    metrics = ['Requests', '4xxErrorRate', '5xxErrorRate']

    # 統計データに応じたStatisticsの値
    statistics = {
        'Requests': 'Sum',
        '4xxErrorRate': 'Average',
        '5xxErrorRate': 'Average'
    }

    results = {}
    for metric_name in metrics:
        try:
            # CloudWatchメトリクスの統計データを取得
            response = cloudwatch.get_metric_statistics(
                Namespace='AWS/CloudFront',
                MetricName=metric_name,
                Dimensions=[
                    {'Name': 'DistributionId', 'Value': DISTRIBUTION_ID},
                    {'Name': 'Region', 'Value': 'Global'}
                ],
                StartTime=start_time,
                EndTime=end_time,
                Period=3600,
                Statistics=[statistics[metric_name]]
            )
            # 結果を辞書に格納
            if 'Datapoints' in response:
                sorted_datapoints = sorted(response['Datapoints'], key=lambda x: x['Timestamp'])
                results[metric_name] = [dp_to_json(dp) for dp in sorted_datapoints]
            else:
                results[metric_name] = []

        except Exception as e:
            results[metric_name] = f"Error: {str(e)}"

    message = construct_message(results)
    try:
        # LINEにメッセージを送信
        send_line_message(LINE_ACCESS_TOKEN, LINE_USER_ID, message)
        return {'statusCode': 200, 'body': json.dumps('Message sent to LINE successfully')}
    except Exception as e:
        return {'statusCode': 500, 'body': json.dumps(f'Failed to send message to LINE: {str(e)}')}

# タイムスタンプをJSTに変換する関数
def dp_to_json(dp):
    jst_time = dp['Timestamp'].astimezone(timezone(timedelta(hours=+9))).strftime('%Y-%m-%d %H:%M:%S')
    return {
        'Timestamp': jst_time,
        'Sum': dp.get('Sum', dp.get('Average', ''))
    }

# LINEに送信するメッセージ
def construct_message(results):
    message = "CloudFrontの統計データを取得しました:\n"
    for metric_name, datapoints in results.items():
        if isinstance(datapoints, list) and datapoints:
            message += f"{metric_name}:\n"
            for dp in datapoints:
                message += f"Timestamp: {dp['Timestamp']}, Sum: {dp['Sum']}\n"
        else:
            message += f"{metric_name}: データなし\n"
        message += "\n"
    return message

# LINEにメッセージを送信する関数
def send_line_message(token, user_id, message):
    url = 'https://api.line.me/v2/bot/message/push'
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {token}'
    }
    data = {
        'to': user_id,
        'messages': [{'type': 'text', 'text': message}]
    }
    response = requests.post(url, headers=headers, data=json.dumps(data))
    response.raise_for_status()

Lambdaレイヤーの作成

  • requestsライブラリを使用するためにLambdaレイヤーとして登録するzipファイルを作成する

環境

$ lsb_release -d
Description:	Ubuntu 22.04.3 LTS
$ python -V
Python 3.10.12
$ pip --version
pip 24.1.2 from /home/user/.pyenv/versions/3.10.12/lib/python3.10/site-packages/pip (python 3.10)

実行コマンド

$ mkdir ./lambda_work ; cd $_
$ pip install -t ./python requests
$ zip -r requests.zip python/
  • AWSマネジメントコンソールからLambda > レイヤー > レイヤーの作成をクリック

  • 以下の情報を入力し、作成をクリック

    • 名前:適当な名前を入力
    • .zipファイルをアップロードをクリックし作成したrequests.zipをアップロード
    • 互換性のあるアーキテクチャ:x86_64
    • ランタイム:Python 3.12 create_layer

Lambda関数の作成

  • AWSマネジメントコンソールからLambda > 関数 > 関数の作成をクリック

  • 一から作成を選択し、以下の情報を入力する

    • 関数名:適当な関数名を入力
    • ランタイム:Python 3.12
    • アーキテクチャ:x86_64
    • 実行ロール:既存のロールを選択する(前段で作成したIAMロールを選択) create_function
  • コードを貼り付けDeployをクリック set_function

  • 画面下部のレイヤー > レイヤーを追加をクリック add_layer

  • 以下の情報を入力し、追加をクリック

    • レイヤーソース:カスタムレイヤー
    • カスタムレイヤー:requests.zipを登録したレイヤーを選択
    • バージョン:1 set_layer
  • レイヤーを登録後、testをクリック create_test

  • テストイベントの設定画面が表示されるので、適当な名前を入力し、その他はデフォルトで保存をクリック set_test

  • 再度テストを実行し、以下のようにMessage sent to LINE successfullyと表示され、CloudFrontのメトリクス情報がLINEに通知されていたらOK run_test

ここまでで、メインの処理となるCloudFrontのメトリクス情報をLINEで通知するLambda関数の設定は完了です。

EventBridgeでLambdaを定期実行する設定を作成する

最後に作成したLambda関数を毎日9時に実行するEventBridgeの設定を追加します。

  • 作成したLambda関数を開き、トリガーを追加をクリック add_trigger

  • 以下の情報を入力し、追加をクリック

    • ソースを選択:EventBridge(CloudWatch Events)
    • ルール:新規ルールを作成
    • ルール名:適当な名前を入力
    • ルールタイプ:スケジュール式
    • スケジュール式:cron(00 0 * _ ? _) ※毎日9時にLambda関数を実行する set_trigger

【備考】cron式フィールドの各項目は左から順に以下の通りで設定する。

月と曜日のフィールド両方でワイルドカード*は使用できないため、どちらかは?とする。

フィールド ワイルドカード
0-59 , - * /
時間 0-23(UTCで指定) , - * /
1-31 , - * ? / L W
1-12またはJAN-DEC , - * /
曜日 1-7またはSUN-SAT , - * ? L #
1970-2199 , - * /

[参考]

https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-cron-expressions.html

  • 定刻になってLINEの通知が飛んでいればOK result

以上で全ての設定が完了です。

終わりに

欲しかった情報が手軽にLINEへ通知されてとても便利になりました。

今回は手動で設定しましたが、SAMやTerraform等のIaC化もできそうなので、今後やってみたいと思います。