Goでslackに通知するバッチを作った【新Webhook】

## はじめに

前回の[AWSの利用料金をChatworkに通知してくれるバッチをGoで作った](https://qiita.com/maika_kamada/items/17ac04a6b8b3c56c4b18)では、取得してきたコストをChatworkに通知するバッチを作りましたが、slackにも飛ばしたくなったので、slackバージョンを作ってみました！

そして、今回はLambdaとCloudWatch EventsをServerless Frameworkで作成してみました！

また、Webhookの旧方式から変わったところもご紹介します。

## 構成図
![CostExplorer-slack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/e07c4c71-3ad7-282e-7fd1-a5225eead20e.png)


当記事では、以下の部分にフォーカスしています。

+ Serverless Frameworkを使って作成したCloudWatch Events、Lambda
+ slackへの通知（Lambdaにアップするコード）

その他のCost Explorerのを使ってのコストの取得方法等は[こちら](https://qiita.com/maika_kamada/items/17ac04a6b8b3c56c4b18)をご覧ください。

## Serverless Frameworkってなに？
簡単に言うと、AWS、Azure、Google Cloudなどに簡単にサーバーレスアプリケーションを構築できるCLIツールです。

どんな作業ができるのか、今回の構成で言うと、本来GUIでLambdaを作成しzipしたコードをアップロードして、その後トリガーを追加しCloudWatch Eventsを作成するというポチポチが、設定ファイルを書いてターミナルからコマンドを叩くだけで一気にできる！というものです。
## いざ作成
### 事前準備
以下の作業は既に終わっているという前提で進めていきます。

+ Goインストール
+ Goのパッケージ管理ツールdepインストール（なくてもできます）
+ AWS CLIのインストール、設定

### Serverless Frameworkのインストール
```
$ npm install -g serverless
```

### プロジェクト作成
`${GOPATH}/src/`配下にプロジェクトを作成する。

```
$ cd ${GOPATH}/src
$ sls create -u https://github.com/serverless/serverless-golang/ -p cost-explorer
```


プロジェクトの設定を行うために、ディレクトリを移動しておきます。

```
$ cd ./cost-explorer
```

### serverless.ymlの設定
serverless.yml中の以下の部分を設定する。

+ リージョン
+ Lambdaに設定するIAMロール
+ CloudWatch Events

#### リージョンの設定
```serverless.yml
# you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
```
東京リージョンに設定しました。

#### Lambdaに設定するIAMロールの設定
```serverless.yml
# you can add statements to the Lambda function's IAM Role here
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ce:*"
      Resource: "*"
```
Cost Explorerのフルアクセスを付与しました。

#### CloudWatch Eventsの設定
```serverless.yml
functions:
  slack:
    handler: bin/main
    events:
      - schedule: cron(00 08 * * ? *)
```

毎日17時に通知されるように設定しました。
UTCなので-9時間で表記します。

### main.goの実装

#### 必要なもの

+ Webhook URL（https\://hooks.slack.com/services/〜/〜）<br>
取得方法は以下の記事を参考にさせていただきました。<br>
[あえて言おう、Slackはデータベースではないと！！【Slack】【Google Form】](https://qiita.com/shuhey_fujiwara/items/99dce403181119bb34bd)


#### 作りたいJSON
slack apiに飛ばすためには、このJSONの形にしたいのです…。見た目はすごく簡単なのに、`json.Marshal()`してもJSON形式にならなかったり、配列に挿入するときの構造体の型の指定が悪かったり…初心者にはなかなか大変でした。

```JSON
{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*AWS利用料金:genie:*"
      }
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "本文"
      }
    }
  ]
}
```
今回はシンプルな通知文になっていますが、様々なレイアウトが可能です！
[slack api Creating rich message layouts](https://api.slack.com/messaging/composing/layouts)

このblocksを使用する方法が2019年はじめに導入された新方式のようです。旧方式では細かい設定を行なっていたattachmentsは、現在の新方式ではレガシーとなり、非推奨となっています。

#### ソースコード（slackへの通知箇所のみ）

```main.go
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"

	"github.com/pkg/errors"
	"github.com/aws/aws-lambda-go/lambda"
)

var (
	IncomingUrl string = "＜Webhook URL＞"
)

type Slack struct {
	Blocks []Block `json:"blocks"`
}

type Block struct {
	Type string `json:"type"`
	Text Text `json:"text"`
}

type Text struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

// メッセージの通知
func postMessage(msg string) error {

	// タイトルブロック
	textMapTitle := Text{}
	textMapTitle.Type = "mrkdwn"
	textMapTitle.Text = "*AWS利用料金:genie:*"

	blockMapTitle := Block{}
	blockMapTitle.Type = "section"
	blockMapTitle.Text = textMapTitle

	// 本文ブロック
	textMapText := Text{}
	textMapText.Type = "mrkdwn"
	textMapText.Text = msg

	blockMapText := Block{}
	blockMapText.Type = "section"
	blockMapText.Text = textMapText

	// 配列に挿入
	blocks := []Block{}
	blocks = append(blocks, blockMapTitle)
	blocks = append(blocks, blockMapText)

	slackMap := Slack{}
	slackMap.Blocks = blocks
	// JSON形式に変換
	params, _ := json.Marshal(slackMap)

	resp, err := http.PostForm(
		IncomingUrl,
		url.Values{"payload": {string(params)}},
	)
	if err != nil {
		fmt.Println("HTTPリクエストに失敗しました。, err:" + fmt.Sprint(err))
		return errors.WithStack(err)
	}

	defer resp.Body.Close()
	contents, _ := ioutil.ReadAll(resp.Body)
	fmt.Printf("Http Status:%s, result: %s\n", resp.Status, contents)

	return nil
}

/**************************
    処理実行
**************************/
func run() error {
	log.Println("----- メッセージの作成 実行")
	msg := MakeMassage(costMonthly, costDaily) //本文の作成
	log.Println("----- メッセージの作成 完了")

	log.Println("----- メッセージの通知 実行")
	err := postMessage(msg)
	if err != nil {
		fmt.Printf("%+v\n", err)
		return err
	}

	log.Println("--- メッセージの通知 完了")
	return nil
}

/**************************
    メイン
**************************/
func main() {
	lambda.Start(run)
}

```
### 必要なパッケージの取得
Goのパッケージ管理ツールdepを使っているので、`$ dep init`で完了。
`$ go get ~`でもできます。

### ビルド
```
$ GOOS=linux GOARCH=amd64 go build -o bin/main
```

### デプロイ
```
$ sls deploy -v
```

### その他の設定

旧方式では、チャンネル名、アカウント名、アイコン画像などはJSONで指定していたものが優先されていたみたいですが、新方式では、チャンネル名、アカウント名、アイコン画像は、[Webhookの設定画面](https://api.slack.com/)からの設定のみとなりました。

Webhook URLを発行した時にメッセージを送るチャンネルを選択する必要があり、チャンネルごとにURLが発行されるようになっています。
<img width="973" alt="スクリーンショット 2020-01-14 18.05.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/62ca5aa4-3ef0-a3ce-6eb8-627012908f4b.png">


アカウント名、アイコン画像は以下のページで設定可能です。
<img width="974" alt="スクリーンショット 2020-01-17 18.20.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/a4d75b13-e0c6-b14a-b66a-e2feb8f6bb69.png">
<img width="972" alt="スクリーンショット 2020-01-17 18.21.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/b966c3c3-ecd5-b0ca-9de4-584a15b323e3.png">

## 実行結果
awsマンが教えてくれるようになりました！:clap:
<img width="590" alt="cost-explorer-slack.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546572/5c4f41f0-c7c6-56e6-6ca7-cc3611ece721.png">

## おわりに

無事slackにも通知することができました！実装するにあたり、JSON形式にするのが難しかったです。最初に宣言する構造体の型の大事さを実感しました…。あとは、`json.Marshal()`をむやみやたらとしない！途中でしてしまうと型が変わってしまうので上手くいきませんでした。最後にする。大事です。

今回は先輩にすごく勧めていただいたServerless Frameworkを使ってみました。初めて使いましたが、`serverless.yml`ファイルを設定するだけなので、思ったよりも簡単でした！

これでChatworkとslackへの通知ができるようになったので、通知シリーズも終了にします。ただ、AWSの合計使用料金だけでなく、サービスごとの詳細料金もわかった方が良いというお声をいただきましたので、いつか実現したいと思っています！引き続き、Goの勉強は続けていきます！