12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambda コンテナイメージを使ってSlackのグラフ貼り付けBOTを作る

Last updated at Posted at 2023-12-22

この記事はWano Group Advent Calendar 2023の23日目の記事となります。

はじめに

プロダクトに関わる指標を確認する際、開発部門が作成した社内ページ・BIツール・各種アナリティクスサービスを用いると思います。

ですが、チャットツール(Slack等)で指標を確認できると、より手軽で利便性も上がるかなと思ったのでAWSサービスを用いて作ってみました。

構成

構成は以下のようになります。

lambda with container.drawio (1).png

  1. AWS Lambda用のコンテナイメージをECRで用意
  2. AWS LambdaでAthenaからデータ取得->グラフ作成→Slackへ通知
  3. 定期的な実行をEventBridge Schedulerで作成

活用するデータはS3に格納されていて、Athenaでクエリが実行できる環境が整っている事を前提とします。

Slack App 設定

まずはSlack apiからアプリを作ります。
作成過程は省きますが、アプリのOAuth & PermissionsのBot User OAuth Tokenの値をメモしておき、Scopeはchat:write,files:write,incoming-webhookを付与しておきます。

通知したいチャンネルにアプリを追加しておきます。

image.png

また、メモしたBot User OAuth Tokenの値をAWS Secrets Managerに登録しておきます。

20231112.png

AWS Lambda用プログラム作成

ローカル環境で実行をテストするためにpython-lambda-localを使います。

$ pip install python-lambda-local

Lambdaで実行する流れの詳細は

  1. Athenaで実行するクエリを読み込む
  2. クエリを実行しデータを取得する
  3. データをもとにMatplotlibでグラフを作成する
  4. 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時に

  1. Github ActionsでS3へアップロード
  2. 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

image.png

無事実行されました。
ローカルでのテストにはaws cliにて用いるcredentialsの情報を~/.aws/credentialsに置いておく必要があり、そのcredentialsのロールが実行するための権限を満たす必要が有ります。

ECRへイメージをデプロイ

ローカルでの実行が成功したら、そのままDockerイメージを作成しECRへデプロイします。

AWS LambdaではLayerをつかってライブラリを追加することもできますが、今回必要なライブラリ群はLayerのサイズ上限「解凍後250MB以内」に抑えることができないので、コンテナイメージを使っています。

コンテナイメージであればイメージサイズ10GBまで対応しているので、問題無くライブラリを使えます。

基本的に流れは

上記リンクに則っています。

Dockerfile作成

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

できました。

202311121.png

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です!

image.png

EventBridge Schedulerで定期実行

EventBridge Schedulerを使って、毎日実行させる場合は、作成したLambda関数に対してevent.jsonの中身を渡してあげればよいです。

EventBridge->スケジュール->スケジュールを作成で任意の時間設定にして

ターゲットをAWS Lambda Invoke
image.png

Lambda関数を選択し、ペイロードにevent.jsonの内容を張り付け。

image.png

これで定期的に結果を張り付けてくれるようになります。

image.png

さいごに

重要な指標についてこの方法でSlackへ投稿することで、全員の指標に対する意識を高めることができるかなと思い、作ってみました。

出力結果などを凝りたい場合、Lambdaでは結果に応じて描画を振り分けることが難しくなってくると思いますし、BIツールが適しているので、そちらで行うのが良いと思います。
あくまで重要な指標についてだけ行うのが、運用面でも良いと思っています。

仮に複数のクエリが対象となる場合は、AWS Step Functions Distributed Mapを用いてそれぞれのevent.jsonを作り、並列に実行させるのが良さそうです。

閲覧された方の参考になれば幸いです。

余談

人材募集

現在、Wanoグループでは人材募集をしています。興味のある方は下記を参照してください。
Wano | Wano Group JOBS
TuneCore Japan | TuneCore Japan JOBS

12
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?