SlackでCloudWatchメトリクスを表示するAWS Chatbot ConfigurationをTerraformで自動構築する

# はじめに
CloudWatchメトリクスは色々な情報を一画面で加工して表示することができる素晴らしいサービスだが、そのために都度AWSコンソールにログインしたりコンソールをごちゃごちゃいじって表示を復元させたり、それを回避するにはJSONを打ち込みなおしたりしなければいけないという面倒くささがある。
もっと簡単にサクッとメトリクスを見ることができないか考えて、Slackで定期で確認してしまえばいいじゃない、という結論が出た。
ということで今回は、SlackでCloudWatchメトリクスのグラフ表示を定期実行するまでを自動化する。

前提知識は以下の通り。
- CloudWatchメトリクスの基本的な知識がある
- Terraformの基本的な知識がある

# AWS Chatbotの作成に必要なTerraformプロバイダ
AWS ChatbotはGoSDKが対応していないため、Terraformの標準AWSプロバイダでは提供されていない。
ただし、[AWS Cloud Control APIをラップしたAWSCC](https://registry.terraform.io/providers/hashicorp/awscc/latest)というプロバイダがあるため、今回はこれを活用する。

以下のようにawsccのprovider設定を入れておけば良い。
バージョン指定が雑なのは気にしないでいただければ。

```HCL
################################################################################
# Provider                                                                     #
################################################################################
provider "aws" {
  region = "ap-northeast-1"
}

provider "awscc" {
  region = "ap-northeast-1"
}
```

# AWS Chatbotを使う前の事前作業
ChatbotからSlackにアクセスできるよう認証をしておく必要がある。
ここだけはどうしても自動化できなかったので、AWSのマネージメントコンソールから設定しよう。

AWS Chatbotのコンソール画面で以下のボタンを押し
![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/9013a5c4-7cd2-8af6-342b-b9f3f923bfb3.png)

その後表示されるダイアログで、クライアントの種類で「Slack」を選択して「設定」を押下する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/6b1d377b-463d-bdaf-c41c-02a49659a70a.png)

成功すると、以下のように表示される。
![キャプチャ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/6945b57c-8be1-617e-8686-e8bada7a7a61.png)

# AWS ChatbotのTerraform記述
以下のように設定する。

```HCL
################################################################################
# Chatbot                                                                      #
################################################################################
resource "awscc_chatbot_slack_channel_configuration" "example" {
  configuration_name = "example"

  slack_workspace_id = "XXXXXXXXXXX"  // ①
  slack_channel_id   = "YYYYYYYYYYY"  // ②

  iam_role_arn = aws_iam_role.chatbot.arn
  guardrail_policies = [
    aws_iam_policy.chatbot_guardrail.arn,
  ]

  logging_level = "INFO"
}
```

```slack_workspace_id```と```slack_channel_id```は、SlackのURLの以下の部分に該当するので、コピペしよう。

```
https://app.slack.com/client/XXXXXXXXXXX/YYYYYYYYYYY
                                 ①            ②
```

AWS側からイベントトリガで投稿する場合はSNSトピックも必要になるが、今回のSlack側からAPIを実行するユースケースでは不要だ。

# IAMロールの設定
IAMは以下のように設定する。
Chatbot用のサービスポリシと、チャンネルで何ができるかというガードレールポリシというものが必要だ。
サービスポリシ ⊃ ガードレールポリシという包含関係らしく、今回チャンネル側で必要になる```cloudwatch:GetMetricWidgetImage```をサービスポリシから外したらエラーになったので、面倒だが両方に入れておこう。
逆に、チャンネル側ではCloudWatchは不要であるため、ガードレールポリシからは外してある。

```HCL
resource "aws_iam_role" "chatbot" {
  name               = local.iam_role_name
  assume_role_policy = data.aws_iam_policy_document.chatbot_assume.json
}

data "aws_iam_policy_document" "chatbot_assume" {
  statement {
    effect = "Allow"

    actions = [
      "sts:AssumeRole",
    ]

    principals {
      type = "Service"
      identifiers = [
        "chatbot.amazonaws.com",
      ]
    }
  }
}

resource "aws_iam_role_policy" "chatbot" {
  name   = local.iam_policy_name
  role   = aws_iam_role.chatbot.name
  policy = data.aws_iam_policy_document.chatbot_custom.json
}

data "aws_iam_policy_document" "chatbot_custom" {
  statement {
    effect = "Allow"

    actions = [
      "cloudwatch:GetMetricWidgetImage",
      "logs:PutLogEvents",
      "logs:CreateLogStream",
      "logs:DescribeLogStreams",
      "logs:CreateLogGroup",
      "logs:DescribeLogGroups",
    ]

    resources = [
      "*",
    ]
  }
}

resource "aws_iam_policy" "chatbot_guardrail" {
  name   = local.iam_chatbot_guardrail_policy_name
  policy = data.aws_iam_policy_document.chatbot_guardrail.json
}

data "aws_iam_policy_document" "chatbot_guardrail" {
  statement {
    effect = "Allow"

    actions = [
      "cloudwatch:GetMetricWidgetImage",
    ]

    resources = [
      "*",
    ]
  }
}
```

# 実行
上記で```terraform apply```実行後、Slack側で以下のように入力してみよう。

```
@aws cloudwatch get-metric-widget-image --region us-east-1 --metric-widget '{
   "view": "timeSeries",
   "stacked": false,
   "metrics": [
       [ "AWS/Billing", "EstimatedCharges", "Currency", "USD", { "period": 21600, "stat": "Maximum" } ]
   ],
   "width": 1576,
   "height": 200,
   "start": "-PT672H",
   "end": "P0D"
}'
```

すると、こんな感じでグラフが表示された！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/dcca767c-6b7b-fb0c-4259-57f8de012857.png)

ついでによく分からないサジェストが毎回表示されてうざいのでこれを消したいが、消し方がよく分からなかった……

~~ちなみに、[以前の記事で紹介したクロスアカウントのメトリクス取得](https://qiita.com/neruneruo/items/454dc01c489d11d76ee8)についても、設定を入れておけば```cloudwatch get-metric-widget-image```で取得できるところまでは試してみた。~~
やっぱりダメだった。クロスアカウントのメトリクス取得をするには、対象アカウントの```CloudWatch-CrossAccountSharingRole```にAssumeRoleする必要があるが、ChatbotではSTS関連の操作が封じられている上に一つの命令内でセッションを変えることもできないので、Chatbotでの実現は不可能だった。

念のため、ガードレールポリシで指定していないコマンドを実行しようとすると、しっかりエラーになってくれた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/9d795df8-2d66-d19d-11e0-0fa8d452bb3c.png)

# ここで問題発生
時限実行には、Slackのリマインド機能を使えば問題ないと考えていたが、Slackのリマインド機能ではどうしても「リマインド:」という文言が入ってしまい、Chatbotがこれを解釈できずに実行エラーになってしまう。余計なリソースを作りたくないので、Slack機能で完結したいが、この「リマインド:」を消す手段、無いのだろうか……。

# 解決策
結局、Slackのリマインド機能は使えないので、以下のようなEventBridgeのスケジュール機能を利用する。
EventBridge Schedulerは、SlackのAPI Destinationsが使え無さそうなので、従来のEventBridgeのスケジュールルールを使う。

## 事前準備
API Destinationsを使用するには、Slack側の設定が必要になる。
[クラスメソッド先生の記事](https://dev.classmethod.jp/articles/event-bridge-api-destinations-with-slack/)を参考に、追加でボットを作っておいて、OAuth用のトークンを取得しておこう。
OAuth用トークンは```var.slack_api_key```で指定できるようにしておく。

## IaCを書く
EventBridgeに関連する情報を以下のように定義する。

input_transformerにはターゲットに送る情報を記載する。
ここでは```username```と```icon_emoji```を変えることで、メッセージ通知元の見た目を変えることができる。
本文でメンションを送る場合は```<>```でユーザ名を囲めば良い。これで囲っておかないととチャットボットが反応してくれない。

事前に取得したボット用のOAuth用トークンは、```aws_cloudwatch_event_connection```の```auth_parameters```ブロックで設定する。ここは認証用のヘッダ情報を設定するので、```Authorization```ヘッダの先頭に```Bearer ```を付与しないといけないので忘れないよう気を付けよう。

```HCL
resource "aws_cloudwatch_event_rule" "example" {
  name = local.eventbridge_rule_name

  schedule_expression = "<任意の起動周期を設定>"

  is_enabled = true
}

resource "aws_cloudwatch_event_target" "example" {
  rule = aws_cloudwatch_event_rule.example.name
  arn  = aws_cloudwatch_event_api_destination.example.arn

  role_arn = aws_iam_role.eventbridge.arn

  input_transformer {
    input_template = jsonencode({
      channel : var.slack_channel_id,
      username : "料金お知らせ定期便",
      icon_emoji : ":dollar:",
      text : "<@aws> cloudwatch get-metric-widget-image --region us-east-1 --metric-widget ${templatefile("./15_metric_widget_image_template.json", {})}"
    })
  }
}

resource "aws_cloudwatch_event_api_destination" "example" {
  name                             = "slack"
  invocation_endpoint              = "https://slack.com/api/chat.postMessage"
  http_method                      = "POST"
  invocation_rate_limit_per_second = 1
  connection_arn                   = aws_cloudwatch_event_connection.example.arn
}

resource "aws_cloudwatch_event_connection" "example" {
  name               = "slack-connection"
  authorization_type = "API_KEY"

  auth_parameters {
    api_key {
      key   = "Authorization"
      value = "Bearer ${var.slack_api_key}"
    }
  }
}
```

これを動かすと、時限周期が来たタイミングで以下のように表示される。
![キャプチャ4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/08391b1f-b128-3c29-1ef3-54562299228d.png)
これで、Chatbotだけに閉じて対応できないのがややイケていないが、時限で好きな情報をSlackに送れるようになった！