「GoとAWS CDKで作る本格SlackBot入門」を読んで、自分でもCloudWatch AlarmをSlack通知してみる

## はじめに
先日開催された技術書典7の 「**GoとAWS CDKで作る本格SlackBot入門**」 という本の内容を受けて自分でもやってみました！本の内容に従いつつ、自分で少しアレンジしています。今回はツイートしたようにEC2のディスク使用量をSlack通知するBotを作ってみました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">AWS CDKの勉強かねて、友達のこの本参考に実践してみた。せっかくだからqiitaにアウトプットする <a href="https://t.co/Akw5CQkmHe">https://t.co/Akw5CQkmHe</a></p>&mdash; ogady@アニオタエンジニア (@gadyma) <a href="https://twitter.com/gadyma/status/1178710388084338688?ref_src=twsrc%5Etfw">September 30, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>



現在は、下記サイトから、電子版を購入することできます、ぜひ今回の記事を読んで興味を持っていただけたら、購入してみてください。
AWS CDK、Goなどの技術を用いて、ChatOps(Slackなどのチャットを用いて快適に開発・運用していこー的なやつ)を導入することの素敵さが存分に味わえる本です。

- 購入先(BOOTH)：[GoとAWS CDKで作る本格SlackBot入門](https://booth.pm/ja/items/1569932)

## 前提
- SlackAPIの用意ができていること
- AWSアカウントとAWS CLIの設定ができていること
- nodeの開発環境ができていること
- Goの開発環境ができていること

## 技術スタック
- Go 1.13
- node 12.10.0
- npm 6.11.3
- typescript 3.6.3
- AWS CLI 1.16.234
- AWS(Lambda、SNS、CloudWatch)
- AWS CDK(TypeScript) 1.9.0

## AWS CDKとは
AWS CDK(Cloud Development Kit)は、CloudForamationテンプレートを、TypeScriptやJavaScript、Java、Python、.NETで生成することができます。

### AWS CDKのメリット
- 生のCloudForamationで書くよりも**記述量を減らすことができる**
- コンポーネント分割が用意なので、**保守性の高いインフラ設計が可能**
- **アプリケーション開発で使っている便利ツールをインフラのコードでも使用できる**

## AWS CDKの導入
```sh:CDKの導入
npm install -g aws-cdk
```
```sh:CDKプロジェクトの作成
// プロジェクトのディレクトリに移動して
cdk init app --language=typescript
```
これを実行すると、以下のようにディレクトリが作成されます。

```txt:プロジェクトディレクトリ
.
├── README.md
├── bin
│     └── alert_ec2_disk_used.ts
├── cdk.json
├── jest.config.js
├── lib
│     └── alert_ec2_disk_used-stack.ts
├── node_modules
├── package-lock.json
├── package.json
├── test
│     └── alert_ec2_disk_used.test.ts
└── tsconfig.json
```

また、CDKのソースがCloudFormationテンプレートとなるため、S3バケットが必要ですが、これは事前にcdkの用意されたコマンドを実行することで CDK Toolkit StackとしてAWS環境にデプロイすることができます。

```sh:CDKToolkitStackをAWS環境にデプロイ
cdk bootstrap
```

CDKを使うにあたって使用するAWSサービスに応じたライブラリをインストールします。

```sh:必要なライブラリをインストールする
npm install @types/node@8 @aws-cdk/aws-lambda @aws-cdk/aws-sns @aws-cdk/aws-sns-subscriptions
```

## 1. CDKを書いてみる

### lib/alert_ec2_disk_used-stack.tsを編集する
ここでは 

- Stack・・・デプロイの単位、リージョン、AWSアカウント
- Constracts・・・デプロイするリソースの定義 

を定義します。

#### Lambda周りの定義
```ts:lib/alert_ec2_disk_used-stack.ts(Lambda周りの定義)
// Lambda Function定義
import { Function, Runtime, Code } from "@aws-cdk/aws-lambda"
import { Topic, Subscription, SubscriptionProtocol } from '@aws-cdk/aws-sns';
import { LambdaSubscription } from '@aws-cdk/aws-sns-subscriptions';

/*
  中略
*/

const lambdaFunction: Function = new Function(this,
      "alert_ec2_disk_used_func", {
      functionName: "alert_ec2_disk_used_func",
      runtime: Runtime.GO_1_X,
      code: Code.asset("./lambdaSrc"),
      handler: "handler",
      memorySize: 128,
      timeout: cdk.Duration.seconds(10),
      environment: {
        "BOT_USER_TOKEN": "{SlackのBot User OAuth Access Token}",
        "CANNEL_ID": "{送信対象のSlackチャンネルID}",
        "MESSAGE": "ディスク使用量が75％を超えています。",
      },
    })

// Lambdaサブスクリプションの定義
const lambdaSub: LambdaSubscription = new　LambdaSubscription(lambdaFunction)
lambdaSub.bind(snsTopic_Over75)
```

#### SNS周りの定義
```ts:lib/alert_ec2_disk_used-stack.ts(SNS周りの定義)
// SNSトピック定義
const snsTopic_Over75: Topic = new Topic(this,
  "ec2_disk_used_over_75", {
  displayName: "alert_ec2_disk_used_over_75",
  topicName: "alert_ec2_disk_used_over_75",
  })
  
// SNSサブスクリプション定義
const snsSub_Over75: Subscription = new Subscription(this,
  "ec2_disk_used_over_75_sub", {
    endpoint: lambdaFunction.functionArn,
    topic: snsTopic_Over75,
    protocol: SubscriptionProtocol.LAMBDA,
    })
```
### bin/alert_ec2_disk_used.tsを編集する
ここでは、デプロイの定義(Lambdaソースのコンパイル、実行コマンドなど)を記載します。ここはほとんど本の内容を参考にしています。

```ts:bin/alert_ec2_disk_used.ts
#!/usr/bin/env node
import 'source-map-support/register';
import cdk = require('@aws-cdk/core');
import { AlertEc2DiskUsedStack } from '../lib/alert_ec2_disk_used-stack';
const util = require('util');
const exec = util.promisify(require('child_process').exec);

async function deploy() {
    //Go のソースを build する
    await exec('go get -v -t -d ./lambdaSrc/... && ' +
        'GOOS=linux GOARCH=amd64 ' +
        'go build -o ./lambdaSrc/handler ./lambdaSrc/**.go');

    const app = new cdk.App();
    new AlertEc2DiskUsedStack(app, 'AlertEc2DiskUsedStack');
    app.synth();

    //build 結果のバイナリを消去する
    await exec('rm ./lambdaSrc/handler');
}

deploy()

```

## 2. LambdaのGoソース
今回は特定のSlackチャンネルに「Incomming Webhooks」を利用して通知を投げます。今回はCDKの勉強がしたかったので、ここは割と雑。。。

```go:handler.go(抜粋)
package main

import (
	"context"
	"encoding/json"
	"os"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/labstack/gommon/log"
	"github.com/pkg/errors"
)

type SNSMessages struct {
	AlarmName        string `json:"AlarmName"`
	AlarmDescription string `json:"AlarmDescription"`
	AWSAccountId     string `json:"AWSAccountId"`
	NewStateValue    string `json:"NewStateValue"`
	NewStateReason   string `json:"NewStateReason"`
	StateChangeTime  string `json:"StateChangeTime"`
	Region           string `json:"Region"`
	OldStateValue    string `json:"OldStateValue"`
}

const (
	EC2_DISK_USED_OVER_75_ALERT = "alert_ec2_disk_used_over_75"
)

const (
	GOOD    = "good"
	WARNING = "warning"
	DANGER  = "danger"
)

func main() {
	lambda.Start(noticeHandler)
}

func noticeHandler(ctx context.Context, snsEvent events.SNSEvent) (e error) {
	cannelID := os.Getenv("CANNEL_ID")
	footer := os.Getenv("FOOTER")
	var snsMessages SNSMessages

	for _, record := range snsEvent.Records {
		snsEvent := record.SNS
		snsMessage := snsEvent.Message

		err := json.Unmarshal([]byte(snsMessage), &snsMessages)
		if err != nil {
			log.Error(err)
			return err
		}

		switch snsMessages.AlarmName {
		case EC2_DISK_USED_OVER_75_ALERT:
			color := WARNING
			if err := PostToSlack(os.Getenv("MESSAGE"), color, cannelID, footer); err != nil {
				log.Error(err)
				return err
			}
			return nil
		default:
      return errors.New("想定するTopicではない")
		}
	}
	return nil
}
```
```go:slack_poster.go
package main

import (
	"encoding/json"
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/nlopes/slack"
)

func PostToSlack(message string, color string, channel string, footer string) error {
	api := slack.New(os.Getenv("BOT_USER_TOKEN"))

	attachment := slack.Attachment{
		Color:  color,
		Text:   message,
		Footer: footer,
		Ts:     json.Number(strconv.FormatInt(time.Now().Unix(), 10)),
	}

	_, _, err := api.PostMessage(channel, slack.MsgOptionAttachments(attachment))
	if err != nil {
		fmt.Println(err)
	}
	return nil
}
```

## 3. ビルドとデプロイ

```sh:CDKをビルドする
//.tsファイルを実行可能な.js ファイルにコンパイルします。
npm run build
```

```sh:スタックをデプロイする
cdk deploy
```

```sh:デプロイ完了するとこうなる。
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
 7/7 | 0:04:09 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | AlertEc2DiskUsedStack 

 ✅  AlertEc2DiskUsedStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:434353216964:stack/AlertEc2DiskUsedStack/8a3a46f0-e45c-11e9-b8b3-06680f8688ee
```

```sh:※スタックを削除する場合
cdk destroy {stack名}
```

## 4. 確認してみる
ちゃんとCloudFormation上でデプロイが成功していることが確認できます。
![スクリーンショット 2019-10-02 0.11.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/331794/e7c9914e-8e71-cd92-39f8-90d5333f0020.png)




## やってみて
AWS CDKはすごいですね。CloudFormationでシコシコ書いていたのが、こんな簡単にIaCを実現できるとは。。。
ただ、**CloudFormationとCDKを書く言語の両方の知識が必要**なため、知識ゼロで始めるとなると学習コストが低いわけではないかな、と思います。

とはいえ、アプリ開発と同じ感覚でIDE等の恩恵を受けることができるので、快適に作業することができる点などが本当に魅力的です。マジで。

今回参考にした**「GoとAWS CDKで作る本格SlackBot入門」**では、AWSアカウント登録から、EC2の起動停止を行うSlackBot構築までを一つ一つ進めていくハンズオン形式になっています。
ここら辺の技術をあまり触ったことがない人でもAWS CDK、Go、ChatOpsの素晴らしさを体験することができるので、ここら辺の技術を使ってみたい方には**かなりオススメ**です！**実案件にもすぐに導入することができる！**

## これからやってみたいこと
今回は、すでに構築してある EC2 と CloudWatchAlarm に対し、SNSTopic と Lambda をCDKで構築したので、次はカスタムメトリクス含めてイチからやってみたいです。(やり方はこれから調べる。。。)
あとは、これで一通りのサーバレスアーキテクチャを構築できるようになるところまでやろうと思います。

## 参考にしたもの
- [GoとAWS CDKで作る本格SlackBot入門 - fukubaka - BOOTH](https://booth.pm/ja/items/1569932)
- [API Reference · AWS CDK(公式リファレンス)](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html)
- [CloudWatch上のエラーログをslackに通知する - Qiita](https://qiita.com/yahagin/items/2248287c5285cd1e9201)