この記事はWano Group Advent Calendar 2023の23日目の記事となります。
はじめに
プロダクトに関わる指標を確認する際、開発部門が作成した社内ページ・BIツール・各種アナリティクスサービスを用いると思います。
ですが、チャットツール(Slack等)で指標を確認できると、より手軽で利便性も上がるかなと思ったのでAWSサービスを用いて作ってみました。
構成
構成は以下のようになります。
- AWS Lambda用のコンテナイメージをECRで用意
- AWS LambdaでAthenaからデータ取得->グラフ作成→Slackへ通知
- 定期的な実行をEventBridge Schedulerで作成
活用するデータはS3に格納されていて、Athenaでクエリが実行できる環境が整っている事を前提とします。
Slack App 設定
まずはSlack apiからアプリを作ります。
作成過程は省きますが、アプリのOAuth & PermissionsのBot User OAuth Tokenの値をメモしておき、Scopeはchat:write,files:write,incoming-webhook
を付与しておきます。
通知したいチャンネルにアプリを追加しておきます。
また、メモしたBot User OAuth Tokenの値をAWS Secrets Managerに登録しておきます。
AWS Lambda用プログラム作成
ローカル環境で実行をテストするためにpython-lambda-localを使います。
$ pip install python-lambda-local
Lambdaで実行する流れの詳細は
- Athenaで実行するクエリを読み込む
- クエリを実行しデータを取得する
- データをもとにMatplotlibでグラフを作成する
- requestsライブラリでSlackへグラフ付きの通知を飛ばす
のような形です。
Lambda ローカル開発環境は以下のような構成になっています。
└── lambda_with_container
├── event.json
├── lambda_function.py
├── lib
├── query.sql
└── requirements.txt
event.json
python-lambda-local 実行時の入力内容が入っています。
実際にLambdaで実行する際の入力と同じです。
{
"slack_channel_name" : "#general", -- 投稿するSlackチャンネル名
"bucket" : "bucket_name", -- Athenaへ実行するsqlファイルのバケット
"key" : "/query/query.sql", -- Athenaへ実行するsqlファイルのキー
"x_column" : "request_date", -- 取得したデータのX軸カラム名
"y_column" : "sent_bytes", -- 取得したデータのY軸カラム名
"hue_column" : "elb_name" -- 取得したデータの要素名
}
lib
python-lambda-localを用いて実行をテストする際に用いるライブラリデータが入っています。
matplotlibを使いたい場合は
$ pip install matplotlib -t ./lib
のようにします。
requirements.txt
実行するのに必要なライブラリを記載しています。
pandas==2.1.1
matplotlib==3.8.0
boto3==1.28.68
requests==2.31.0
awswrangler==3.4.1
seaborn==0.13.0
python-lambda-local
用の環境(lib
)を作る際は、これを用いて
$ pip install -r requirements.txt -t ./lib
のようにして作ります。
query.sql
Athenaへ実行するためのクエリを書いています。
SELECT
DATE(SUBSTR(request_timestamp,1,10)) as request_date,
elb_name,
SUM(sent_bytes) as sent_bytes
FROM "sampledb"."elb_logs"
GROUP BY 1,2
実際の動作時は、ローカルへ置かずevent.json
に記載したS3 URIの場所にファイルをアップロードしておきます。
自動でアップロードする方法としては、Githubでコード管理していることを前提として、特定ブランチPUSH時に
- Github ActionsでS3へアップロード
- CodePipelineの設定を行い、CodeBuildでS3へアップロード
等で行います。
lambda_function.py
lambdaで実際に動かすコードです。
import json
import logging
import os
import awswrangler as wr
import boto3
import matplotlib.pyplot as plt
import requests
import seaborn as sns
from botocore.exceptions import ClientError
from matplotlib import rcParams
sns.set()
rcParams['font.family'] = 'Noto Sans CJK JP'
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def get_secret():
secret_name = "SLACK_BOT_USER_OAUTH_TOKEN"
region_name = "ap-northeast-1"
# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(
service_name='secretsmanager',
region_name=region_name
)
try:
get_secret_value_response = client.get_secret_value(
SecretId=secret_name
)
except ClientError as e:
# For a list of exceptions thrown, see
# https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
raise e
# Decrypts secret using the associated KMS key.
secret = json.loads(get_secret_value_response['SecretString'])
# Your code goes here.
return secret['SLACK_BOT_USER_OAUTH_TOKEN']
def download_query_from_s3(bucket, key, local_file_path, session):
try:
wr.s3.download(
path=f"s3://{bucket}{key}",
local_file=local_file_path,
boto3_session=session
)
except ClientError as e:
logger.error(f"Error downloading file from S3: {e}")
raise
def post_image_to_slack(slack_token, channel, image_path):
headers = {"Authorization": f"Bearer {slack_token}"}
payload = {"channels": channel}
with open(image_path, "rb") as image_file:
image_data = image_file.read()
files = {"file": (image_path, image_data, "image/png")}
try:
response = requests.post("https://slack.com/api/files.upload", headers=headers, data=payload, files=files)
response.raise_for_status()
except requests.exceptions.RequestException as e:
logger.error(f"Error posting to Slack: {e}")
raise
def lambda_handler(event, context):
slack_token = get_secret()
slack_channel = event['slack_channel_name']
local_save_image_path = '/tmp/result.png'
session = boto3.Session()
try:
download_query_from_s3(event['bucket'], event['key'], '/tmp/query.sql', session)
with open('/tmp/query.sql', 'r') as file:
sql = file.read()
df = wr.athena.read_sql_query(sql=sql, database='*', boto3_session=session, ctas_approach=False)
plt.figure(figsize=(15, 6))
if event['hue_column'] != "":
sns.lineplot(df, x=event['x_column'], y=event['y_column'], hue=event['hue_column'])
else:
sns.lineplot(df, x=event['x_column'], y=event['y_column'])
plt.savefig(local_save_image_path)
plt.close()
post_image_to_slack(slack_token, slack_channel, local_save_image_path)
except Exception as e:
logger.error(f"Error in lambda execution: {e}")
raise
return 'Result.png has been saved and posted to Slack.'
python-lambda-localで実行してみます。
$ python-lambda-local -l lib/ -f lambda_handler -t 100 lambda_function.py event.json
無事実行されました。
ローカルでのテストにはaws cliにて用いるcredentialsの情報を~/.aws/credentials
に置いておく必要があり、そのcredentialsのロールが実行するための権限を満たす必要が有ります。
ECRへイメージをデプロイ
ローカルでの実行が成功したら、そのままDockerイメージを作成しECRへデプロイします。
AWS LambdaではLayerをつかってライブラリを追加することもできますが、今回必要なライブラリ群はLayerのサイズ上限「解凍後250MB以内」に抑えることができないので、コンテナイメージを使っています。
コンテナイメージであればイメージサイズ10GBまで対応しているので、問題無くライブラリを使えます。
基本的に流れは
上記リンクに則っています。
Dockerfile作成
FROM public.ecr.aws/lambda/python:3.11
# システム更新と必要なパッケージのインストール
RUN yum update && yum install -y wget unzip
# ファイルコピー
COPY requirements.txt ${LAMBDA_TASK_ROOT}
COPY lambda_function.py ${LAMBDA_TASK_ROOT}
# ライブラリをrequirements.txtからインストール
RUN pip install --no-cache-dir -r requirements.txt
# Matplotlib用に日本語フォントをインストール→ダウンロード
RUN wget https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip && \
unzip NotoSansCJKjp-hinted.zip -d NotoSansCJKjp && \
mkdir -p /usr/share/fonts/ && \
mv NotoSansCJKjp /usr/share/fonts/NotoSansCJKjp && \
chmod 644 /usr/share/fonts/NotoSansCJKjp/* && \
rm NotoSansCJKjp-hinted.zip
# Lambda 関数ハンドラを指定
CMD ["lambda_function.lambda_handler"]
ローカル環境での実行では、設定によりますが問題無く日本語をMatplotlibで使えます。
ただし、Lambda with Containerを使う場合、コンテナに日本語フォントをダウンロードしておく必要があります。
テスト&デプロイ
イメージを作成します。
$ docker build --platform linux/amd64 -t graph-notification:test .
ECRへリポジトリを作成します
$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 111122223333.dkr.ecr.ap-northeast-1.amazonaws.com
Login Succeeded
$ aws ecr create-repository --repository-name graph-notification --region ap-northeast-1 --image-scanning-configuration scanOnPush=true --image-tag-mutability MUTABLE
{
"repository": {
"repositoryArn": "arn:aws:ecr:ap-northeast-1:xxxxxxxxxxxx:repository/graph-notification",
"registryId": "xxxxxxxxxxxx",
"repositoryName": "graph-notification",
"repositoryUri": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/graph-notification",
"createdAt": "2023-11-12T13:16:05+09:00",
"imageTagMutability": "MUTABLE",
"imageScanningConfiguration": {
"scanOnPush": true
},
"encryptionConfiguration": {
"encryptionType": "AES256"
}
}
}
レスポンスのrepositoryUri
をコピーして、先ほど作成したイメージを置き換えます
$ docker tag graph-notification:test <repositoryUri>:latest
ECRリポジトリにイメージをデプロイします。
$ docker push <repositoryUri>:latest
できました。
Lambda with Container 関数作成
IAMロールの作成
Lambda with Container に割り当てるIAMロールを作っておきます。
今回は簡単にコンソールから
- AmazonAthenaFullAccess
- AmazonS3FullAccess
- secretsmanager:GetSecretValueのActionが許可された独自ポリシー
のポリシーを追加しました。実際に運用する場合は最小権限の原則の元ポリシーをアタッチしたほうが良いです。
関数作成&実行
ECRリポジトリにデプロイしたイメージを用いて関数を作成します。
$ aws lambda create-function \
--function-name graph-notification \
--region ap-northeast-1 \
--package-type Image \
--code ImageUri=<repositoryUri>:latest \
--role arn:aws:iam::xxxxxxxxxxxx:role/lambda-ex \ # 先ほど作成したIAMロールのARN
--timeout 60 \
--memory-size 1024 \
--ephemeral-storage Size=1024
{
"FunctionName": "graph-notification",
"FunctionArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:graph-notification",
"Role": "arn:aws:iam::xxxxxxxxxxxx:role/tesrrole",
"CodeSize": 0,
"Description": "",
"Timeout": 60,
"MemorySize": 1024,
"LastModified": "2023-11-12T04:38:00.836+0000",
"CodeSha256": "",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "xxxxxxxxxxxx",
"State": "Pending",
"StateReason": "The function is being created.",
"StateReasonCode": "Creating",
"PackageType": "Image",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
"Size": 1024
}
}
関数を呼び出します。
AWS CLI v2では、呼び出し時のjsonをbase64にエンコードする必要があります。
$ aws lambda invoke --function-name graph-notification --payload "$(cat event.json | base64)" response.json
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
「スッコココ」とくればOKです!
EventBridge Schedulerで定期実行
EventBridge Schedulerを使って、毎日実行させる場合は、作成したLambda関数に対してevent.json
の中身を渡してあげればよいです。
EventBridge->スケジュール->スケジュールを作成で任意の時間設定にして
Lambda関数を選択し、ペイロードにevent.json
の内容を張り付け。
これで定期的に結果を張り付けてくれるようになります。
さいごに
重要な指標についてこの方法でSlackへ投稿することで、全員の指標に対する意識を高めることができるかなと思い、作ってみました。
出力結果などを凝りたい場合、Lambdaでは結果に応じて描画を振り分けることが難しくなってくると思いますし、BIツールが適しているので、そちらで行うのが良いと思います。
あくまで重要な指標についてだけ行うのが、運用面でも良いと思っています。
仮に複数のクエリが対象となる場合は、AWS Step Functions Distributed Mapを用いてそれぞれのevent.json
を作り、並列に実行させるのが良さそうです。
閲覧された方の参考になれば幸いです。
余談
- matplotlibの日本語化については、japanize-matplotlibを使うという手もあります。
- BIツールのTableauでは、バージョン2021.3からSlackへ通知を行うことができますが、Tableauへ登録しているメールアドレス宛にDMで届くので、今回行ったものより少し柔軟性に欠けます。
人材募集
現在、Wanoグループでは人材募集をしています。興味のある方は下記を参照してください。
Wano | Wano Group JOBS
TuneCore Japan | TuneCore Japan JOBS