サーバレス環境でbotを動かす(Go & Cloud Run)

こんにちは！私(@f_yamagami)です！
この記事は、[All About Group（株式会社オールアバウト） Advent Calendar 2019](https://qiita.com/advent-calendar/2019/allabout) クリスマスイブの記事です。

## 前談(The・前談)
クリスマスイブですね。~~マジあれですね~~
みなさんはクリスマスプレゼントに何が欲しいですか？
ゲーム？ お金？ 休み？ 
やっぱりエンジニアならサーバですか？
あれ、そこのエンジニアさん
どうしてサーバと聞いて苦い顔をされていらっしゃるんですか?
え？ サーバ障害の対応で、せっかくのクリスマスディナーが冷えてしまい、
恋人との関係も冷えてしまった過去が？~~ざまあみ(ry~~
※私はサーバにはなんの思い出もないです。

ということで、今回は会社の開発合宿で[先輩エンジニア](https://qiita.com/y_hideshi)に手取り足取りしていただきながら、**サーバレスの環境**でbatch処理を行った話を書いていきます。
[先輩のQiitaはこちら↓]
https://qiita.com/y_hideshi/items/085a92746578b5b7f01d

今回作ったのは、先輩のQiitaにもある通り、
**定期的にGCPの確約利用割引の期日の自動チェック & アラートあげるbot**です。

## 確約利用割引とは

確約利用割引はGCEの契約を1年間または3年間の支払いを確約する代わりに、特定の量の vCPU、メモリ、GPU、ローカル SSD を割引価格で購入できるものです。

マシンタイプやコア数、適用期間などを指定した**コミットメント**というものを購入すると、その分の利用分は確約利用割引が適用されるというものです。
雑な説明にはなりますが、年間パスポートのようなものです。
最初に一括で代金をを支払うわけではないので、通常の年間パスポートとは少し違いますが、下記の例を見るとだいたい年間パスポートみたいなものだという計算になると思います。

**例：8コア用のコミットメントを購入**
- 4コアを使用した月 → 8コア用の確約された利用料金が請求される
- 8コアを使用した月 → 8コア用の確約された利用料金が請求される
- 12コアを使用した月 → 8コア用の確約された利用料金 + 4コア分は通常料金で請求される

詳しくは公式のドキュメントでご確認ください。
https://cloud.google.com/compute/docs/instances/signing-up-committed-use-discounts?hl=ja

#前談2 (準備的な)
まず、定期的にGCPの確約利用割引の期日の自動チェック & アラートあげるbotを作るにあたって、実現したいことの認識合わせを先輩としました。

- 今ある課題
  - 契約している確定利用割引の更新日が近づくと、メールでのアラートは来るけど見落としそう
  - 見落として更新を忘れると通常価格での支払いとなり、損してないけど損した気分になるので嫌だ
- 実現したいこと
  - 契約している確定利用割引の更新日が近づくと、**Slack**で通知させたい！

某総理大臣と某大統領のごとく、直接会談の結果、完全に認識が一致しました。
次に、実現方法について考えました。
確定利用割引のコミットメントはAPIで内容が確認できることがわかったので、
下記のように、私は考えました。

1.下記のような簡単なアプリを作る
　・コミットメントの終了日を取得するAPIを叩いて値を取得
　・取得したコミットメントの終了日が1週間以内か確認
　・1週間以内ならSlackのAPIを叩いて通知

2.上記アプリをどっかのサーバに置いてcronで1日1回実行

私「先輩！ こんな感じで考えてるんですけど、どこのサーバに置きますか？
てか、新しいサーバを立てますか？」

[^1]先輩「なるほど、君はどこのサーバにアプリを置くか迷っているようだけど、**全てが違う。**
サーバをどこに置くか以前に、サーバを立てる必要があるのかを考えたか？
貴様の悩むべきところは、サーバ代の金銭的負担だけでなく、
サーバ管理が必要となり、**技術的負債**まで残してしまうことを、1mmたりとも
考慮できていない**己の無知に気が付いていないことではないのか？**」

あまりの衝撃的なお言葉に、なんとおっしゃっていたのかは正確には忘れてしまいましたが、
実際のところは「え？サーバは使わないでCloud Runでしてみない？」だった気もします。

ということで、今回はサーバレス環境で実装することになりました。
また、私は
- コミットメントの終了日を取得するAPIを叩いて値を取得
- 取得したコミットメントの終了日が近いかの確認
の部分を実装したので、その辺り中心に書こうと思います。
## システム構成
(略)
※先輩のQiitaにて詳細に紹介済み
https://qiita.com/y_hideshi/items/085a92746578b5b7f01d#%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%A7%8B%E6%88%90

## 実装
今回はgo言語で実装しました。

``` main.go

/* 
 * importは省略しています。
 * 愚直に公式のExamplesに沿って認証を通していきます。
 * 認証にApplication Default Credentialsを利用したサンプルです。
 */
ctx := context.Background()

c, err := google.DefaultClient(ctx, compute.CloudPlatformScope)
if err != nil {
  log.Fatal(err)
}

computeService, err := compute.New(c)
if err != nil {
  log.Fatal(err)
}

// プロジェクトとリージョンを指定
project := "my-project"
region := "my-region"

if err := req.Pages(ctx, func(page *compute.CommitmentList) error {

    // 今日の日付
    today_str, _ := time.Parse("20060102", time.Now())

    for _, commitment := range page.Items {
        // 文字列 → 日付型
        endDate, _ := time.Parse("2006-01-02T15:04:05Z07:00", commitment.EndTimestamp)

        // 何日前か比較したいリスト
        dayArray := [3]int{7, 14, 30}
        for _, x := range dayArray {
            // 今日の日付からx日先の日付をcompareDateに設定する
            compareDate := today_str.AddDate(0, 0, x)

            // 日付型を整えた上で、アラートを出すべき日かを確認する。
            if (endDate.Format("2006-01-02") == compareDate.Format("2006-01-02")){
                message_str := "確約利用割引 `" + commitment.Name + "`の期限は `" + endDate.Format("2006-01-02") + "`までです:alert:"
                // pub/subにmessageを公開する
                PubsubFunc(message_str)
            }
        }
    }
    return nil
}); err != nil {
        log.Fatal(err)
}
```

```PubsubFunc関数.go
func PubsubFunc(message_str string) {
    ctx := context.Background()

    client, err := pubsub.NewClient(ctx, "my-client")
    if err != nil {
        log.Fatal(err)
    }

    topicName := "slack-topics"
    topic = client.Topic(topicName)

    exists, err := topic.Exists(ctx)
    if err != nil {
        log.Fatal(err)
    }
    if !exists {
        log.Printf("Topic %v doesn't exist - creating it", topicName)
    }
    // slackに投稿するために必要な要素を設定する
    atr := map[string]string{
        "Color":"warning",
        "Fallback":"メッセージ通知タイトル",
        "AuthorName":"Cloud Run",
        "AuthorSubname":"クラウドラン",
        "AuthorLink":"https://console.cloud.google.com/~~~",
        "AuthorIcon":"https://repository-images.githubusercontent.com/~~~",
        "Text":message_str,
        "Footer":"slackapi",
        "FooterIcon":"https://platform.slack-edge.com/img/~~~.png",
        "WebhookUrl":"https://hooks.slack.com/services/~~~",
    }
    topic.Publish(ctx, &pubsub.Message{Data: []byte("payload"), Attributes: atr})
}
```

※特にmain.goの方で、ネストが深くなっていたり、無理やりな部分があり、見苦しく申し訳ないです。

## 最後に
今まではPHPでコードを書いて、すでに構築されているk8sの環境にリリースしたことぐらいしかなく、
go言語もサーバレス環境も全てが新鮮でとても勉強になりました。

また、PubSubにメッセージを公開する部分は、ほとんど先輩に教えていただきました。
実装をGCRにあげるところや環境周りでも大変お世話になり、とても感謝しております。
ありがとうございました。

[^1]: 私の妄想上での発言です。