はじめに
CloudWatchで取得しているCloudFrontのメトリクス情報を通知するLINE BOTを作ってみました。
CloudFrontへのリクエスト数や、エラー率を簡単に確認したいなと思い、Lambdaの勉強を兼ねて今回はLINE Messaging APIと連携してみました。
通知されるデータ
今回作ったLINE BOTから送られるデータはこちら。
通知するメトリクスデータは以下としました。
メトリクス名 | 統計値 | 期間 | メトリクス内容 |
---|---|---|---|
Requests | Sum | 1時間 | すべての HTTP メソッド、および HTTP リクエストと HTTPS リクエストの両方について CloudFront が受信したビューワーリクエストの総数。 |
4xxErrorRate | Average | 1時間 | レスポンスの HTTP ステータスコードが 4xx であるすべてのビューワーリクエストの割合 (%)。 |
5xxErrorRate | Average | 1時間 | レスポンスの HTTP ステータスコードが 5xx であるすべてのビューワーリクエストの割合 (%)。 |
構成図
今回作成した仕組みの構成図と処理の流れは以下の通りです。
LambdaランタイムはPython 3.12を利用しています。
また、AWSの各サービスはバージニア北部(us-east-1)で作成しています。
理由として、CloudFrontのメトリクスはバージニア北部のリージョンに集約されるため、Lambda等もバージニア北部でないとデータが取得できないからです。
(Lambda関数を東京リージョンで作成してしまい、データが取得できず結構な時間ハマってしまいました…)
Line Messaging APIキーを取得
Line Messaging APIを利用するためには、LINE Developersコンソールからチャネルの作成を行う必要があります。
-
公式サイトの以下の手順より、LINE Developersコンソールからチャネルを作成する https://developers.line.biz/ja/docs/messaging-api/getting-started/
-
チャネルを作成したら、チャネルアクセストークンを発行し、控えておく
-
作成したチャネルのチャネル基本設定欄から、「あなたのユーザID」を控えておく
-
作成したチャネルの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を開き、パラメータストア > パラメータの作成をクリック
-
以下の通り各項目を入力し、パラメータを作成をクリック
- 名前:LINE_ACCESS_TOKEN
- 利用枠:標準
- タイプ:安全な文字列
- KMS キーソース:現在のアカウント
- KMS キー ID:alias/aws/ssm
- 値:発行したチャネルアクセストークン
-
パラメータの一覧に戻るので、LINE_ACCESS_TOKENが作成されていることを確認する
-
再度パラメータの作成をクリックし、同じ要領で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
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とした)を入力し、ポリシーの作成をクリック
IAMロールの作成
作成したポリシーを紐付けたLambda用のIAMロールを作成します。
-
AWSマネジメントコンソールよりIAM > ロール > ロールを作成をクリック
-
信頼されたエンティティタイプで「AWSのサービス」を選択し、ユースケースでLambdaを選択後、次へをクリック
-
先程作成したポリシーにチェックを入れ、次へをクリック
-
適当なロール名(今回はcloudwatch-lambda-test-roleとした)を入力し、ロールの作成をクリック
ここで作成した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
Lambda関数の作成
-
AWSマネジメントコンソールからLambda > 関数 > 関数の作成をクリック
-
一から作成を選択し、以下の情報を入力する
- 関数名:適当な関数名を入力
- ランタイム:Python 3.12
- アーキテクチャ:x86_64
- 実行ロール:既存のロールを選択する(前段で作成したIAMロールを選択)
-
コードを貼り付けDeployをクリック
-
画面下部のレイヤー > レイヤーを追加をクリック
-
以下の情報を入力し、追加をクリック
- レイヤーソース:カスタムレイヤー
- カスタムレイヤー:requests.zipを登録したレイヤーを選択
- バージョン:1
-
レイヤーを登録後、testをクリック
-
テストイベントの設定画面が表示されるので、適当な名前を入力し、その他はデフォルトで保存をクリック
-
再度テストを実行し、以下のようにMessage sent to LINE successfullyと表示され、CloudFrontのメトリクス情報がLINEに通知されていたらOK
ここまでで、メインの処理となるCloudFrontのメトリクス情報をLINEで通知するLambda関数の設定は完了です。
EventBridgeでLambdaを定期実行する設定を作成する
最後に作成したLambda関数を毎日9時に実行するEventBridgeの設定を追加します。
-
作成したLambda関数を開き、トリガーを追加をクリック
-
以下の情報を入力し、追加をクリック
- ソースを選択:EventBridge(CloudWatch Events)
- ルール:新規ルールを作成
- ルール名:適当な名前を入力
- ルールタイプ:スケジュール式
- スケジュール式:cron(00 0 * _ ? _) ※毎日9時にLambda関数を実行する
【備考】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
以上で全ての設定が完了です。
終わりに
欲しかった情報が手軽にLINEへ通知されてとても便利になりました。
今回は手動で設定しましたが、SAMやTerraform等のIaC化もできそうなので、今後やってみたいと思います。