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


はじめに

前回のAWSの利用料金をChatworkに通知してくれるバッチをGoで作ったでは、取得してきたコストをChatworkに通知するバッチを作りましたが、slackにも飛ばしたくなったので、slackバージョンを作ってみました!

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

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

構成図

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

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

その他のCost Explorerのを使ってのコストの取得方法等はこちらをご覧ください。

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の実装

必要なもの

作りたいJSON

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

{
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*AWS利用料金:genie:*"
      }
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "本文"
      }
    }
  ]
}

今回はシンプルな通知文になっていますが、様々なレイアウトが可能です!
slack api Creating rich message 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の設定画面からの設定のみとなりました。

Webhook URLを発行した時にメッセージを送るチャンネルを選択する必要があり、チャンネルごとにURLが発行されるようになっています。

アカウント名、アイコン画像は以下のページで設定可能です。

実行結果

awsマンが教えてくれるようになりました!

おわりに

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

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

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