AWS CloudWatch Metricsのグラフ画像をDiscordに送る（全体ソースコードあり）

# 概要

AWS CloudWatch Metrics でいろいろリソースの状況が確認できますが、Discord に送ることができたら、簡単にスマホでリソース監視ができるので、やってみたいと思います。

# ステップ

## 1. Discord Webhook

Discord Webhook の URL 作成、作ったあとに

:::note warn
https://discord.com/api/webhooks/12345/ABCDE
:::

みたいなリンクが取得できるので、その Webhook URL を使います。

マニュアル：https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks

## 2. CloudWatch Metrics

CloudWatch Metrics にアクセスするして、ほしいグラフを設定してから、Source のタブで直接設定JSONがもらえます。すごく便利な機能でそのまま貼り付けると使えます。もちろん公式ドキュメントもあるので合わせて読んだらもっと理解できると思います。

マニュアル：https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_GetMetricData.html

![Screenshot from 2023-12-04 23-07-22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/893830/adb123e1-ae0f-d615-e10b-be5c756c49b5.png)

## 3. Lambda を作成

CloudWatch Metricsを取得して、Discord Webhook で送る Lambda を作ります。role がわからない／テストの時に、一旦 CloudWatchReadOnlyAccess で良いかもしれません。

# ソースコード紹介

## Metrics の設定
今回は Invocations, Errors, Duration 三つの Metrics を取得

```python
  metrics = {
    "Invocations": [
      [ "AWS/Lambda", "Invocations", "FunctionName", "funtion1", { "period": 900, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion2", { "period": 900, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion3", { "period": 900, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion4", { "period": 900, "stat": "Sum", "region": "ap-northeast-1" } ]
    ],
    "Errors": [
      [ "AWS/Lambda", "Errors", "FunctionName", "funtion1", { "period": 300, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion2", { "period": 300, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion3", { "period": 300, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion4", { "period": 300, "stat": "Sum", "region": "ap-northeast-1" } ]
    ],
    "Duration": [
      [ "AWS/Lambda", "Duration", "FunctionName", "funtion1", { "period": 300, "stat": "Average", "region": "ap-northeast-1" } ],
      [ "...", "funtion2", { "period": 300, "stat": "Average", "region": "ap-northeast-1" } ],
      [ "...", "funtion3", { "period": 300, "stat": "Average", "region": "ap-northeast-1" } ],
      [ "...", "funtion4", { "period": 300, "stat": "Average", "region": "ap-northeast-1" } ]]
    ],
  }
```

## Metrics の設定（続き）

画像の大きさなどの共通設定はこちらに設定

```python
  for metric, _ in metrics.items():

    widget_definition = json.dumps(
      {
        "liveData": False,
        "stacked": False,
        "width": 800,
        "height": 400,
        "start": '-PT3H',
        "end": "P0D",
        "timezone": '+0900',
        "view": "timeSeries",
        "metrics": metrics[metric],
      }
    )
```

## 画像の取得

get_metric_widget_image を使用して画像をローカルに保存

```python
    response = cloudwatch.get_metric_widget_image(MetricWidget = widget_definition)
    image_path = f'/tmp/{metric}.png'

    with open (image_path, 'wb') as f:
      f.write(response["MetricWidgetImage"])
```

## Discord Webhook

Webhook URL を使って、ローカルに保存した画像を Discord に送る

```python
    webhook_url = os.getenv('discord_webhook_url')

    discord = Discord(url=webhook_url)
    with open(image_path, 'rb') as f:
      discord.post(username="monitoring", content=metric, file={ "attachment": f })
```

# 全体ソースコード

```python
import json
import boto3
from discordwebhook import Discord
import time
import os

def lambda_handler(event, context):
  cloudwatch = boto3.client('cloudwatch')

  metrics = {
    "Invocations": [
      [ "AWS/Lambda", "Invocations", "FunctionName", "funtion1", { "period": 900, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion2", { "period": 900, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion3", { "period": 900, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion4", { "period": 900, "stat": "Sum", "region": "ap-northeast-1" } ]
    ],
    "Errors": [
      [ "AWS/Lambda", "Errors", "FunctionName", "funtion1", { "period": 300, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion2", { "period": 300, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion3", { "period": 300, "stat": "Sum", "region": "ap-northeast-1" } ],
      [ "...", "funtion4", { "period": 300, "stat": "Sum", "region": "ap-northeast-1" } ]
    ],
    "Duration": [
      [ "AWS/Lambda", "Duration", "FunctionName", "funtion1", { "period": 300, "stat": "Average", "region": "ap-northeast-1" } ],
      [ "...", "funtion2", { "period": 300, "stat": "Average", "region": "ap-northeast-1" } ],
      [ "...", "funtion3", { "period": 300, "stat": "Average", "region": "ap-northeast-1" } ],
      [ "...", "funtion4", { "period": 300, "stat": "Average", "region": "ap-northeast-1" } ]]
    ],
  }

  for metric, _ in metrics.items():

    widget_definition = json.dumps(
      {
        "liveData": False,
        "stacked": False,
        "width": 800,
        "height": 400,
        "start": '-PT3H',
        "end": "P0D",
        "timezone": '+0900',
        "view": "timeSeries",
        "metrics": metrics[metric],
      }
    )

    response = cloudwatch.get_metric_widget_image(MetricWidget = widget_definition)
    image_path = f'/tmp/{metric}.png'

    time.sleep(1)

    with open (image_path, 'wb') as f:
      f.write(response["MetricWidgetImage"])

    webhook_url = os.getenv('discord_webhook_url')

    discord = Discord(url=webhook_url)
    with open(image_path, 'rb') as f:
      discord.post(username="monitoring", content=metric, file={ "attachment": f })
  
  return {
    'statusCode': 200,
    'body': json.dumps('Hello from Lambda!')
  }
```