Help us understand the problem. What is going on with this article?

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

はじめに

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

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

前提

  • 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の導入

CDKの導入
npm install -g aws-cdk
CDKプロジェクトの作成
// プロジェクトのディレクトリに移動して
cdk init app --language=typescript

これを実行すると、以下のようにディレクトリが作成されます。

プロジェクトディレクトリ
.
├── 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環境にデプロイすることができます。

CDKToolkitStackをAWS環境にデプロイ
cdk bootstrap

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

必要なライブラリをインストールする
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周りの定義

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周りの定義

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ソースのコンパイル、実行コマンドなど)を記載します。ここはほとんど本の内容を参考にしています。

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の勉強がしたかったので、ここは割と雑。。。

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
}
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. ビルドとデプロイ

CDKをビルドする
//.tsファイルを実行可能な.js ファイルにコンパイルします。
npm run build
スタックをデプロイする
cdk deploy
デプロイ完了するとこうなる。
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
 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
※スタックを削除する場合
cdk destroy {stack名}

4. 確認してみる

ちゃんとCloudFormation上でデプロイが成功していることが確認できます。
スクリーンショット 2019-10-02 0.11.00.png

やってみて

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

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

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

これからやってみたいこと

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

参考にしたもの

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした