GCPのFunctionsを使って、サーバレスに定期的に天気情報をSlack通知してみよう。

今回はGCPのscheduler、Functions、VPC、CloudNATを使って、Slackに天気を通知する機能を実装してみようと思います。
GCPのCloudFunctionsを使ってKCCS APIサービスから取得した天気予報を定期的にSlackに通知する記事の第3弾です。

#構成イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1525729/349a7681-ea42-4e68-1403-786c01502610.png)


#各サービスの役割
各サービスの役割は以下とします。

|サービス|役割|
|:---|:---|
|IAM|Cloud Functionの認証用サービスアカウント|
|Cloud Scheduler|処理の起動トリガ 毎朝10:00に起動する|
|Cloud Functions|コントローラ、KCCS APIから気象情報を取得し、Slackに通知する|
|VPC ネットワーク|CloudNATの所属するネットワーク|
|サーバーレスVPCアクセス<BR>(プライベートVPC)|Cloud Functions-Google VPC→Cloud NAT経由で外にでる|
|Cloud NAT|IPアドレスを固定する|
|KCCSAPI(天気情報取得)|Rest APIにて天気情報を取得する|
|Slack|天気情報の通知先|

#実装いろいろ

### IAM
認証用アカウントです。
こちらを作って認証させないとFuncionsをどこからでも叩けるようになってしまいますので、セキュリティ上設定することが好ましいです。
「IAM」→「サービスアカウント」→「サービスアカウントを作成」より、アカウントを作成します。

注意するべきポイントはロールに
**Cloud Functions起動元**
をセットしないと、Functionを呼べませんので、必ずセットしてください。

ここでは
**function-iam**
という名前でつくります。

### Cloud Scheduler
Schduler画面から、「作成」ボタンを押下しジョブを作成します。

|項目名|設定値|備考|
|:---|:---|:---|
|名前|kccs-api-qiita-notify-slack-scheduler|任意で好きな名前を！|
|頻度|0 10 * * *|毎朝10:00に起動|
|タイムゾーン|日本標準時(JST)|-|


ジョブターゲットは

|項目名|設定値|備考|
|:---|:---|:---|
|ターゲットタイプ|HTTP|-|
|HTTPメソッド|POST|-|
|Authヘッダー|OIDCトークンを追加(※)|認証通さないと、外から誰でも使えるので設定することをオススメします。|
|サービスアカウント|function-iam|IAMで払い出したサービスアカウントを指定します。|
|対象(URL)|(※)|※Function作成時に払い出されるので、作ってからセットします。|

### VPC ネットワーク
Cloud NATの所属するネットワークを作成します。
VPC ネットワークの画面から、**VPC ネットワークを作成**を選択し、以下を参照して以下のように入力します。

|項目名|設定値|備考|
|:---|:---|:---|
|名前|fucnction-connect-vpc|任意で好きな名前を！|
|サブネットの名前|fucnction-connect-qiita-subnet|任意で好きな名前を！|
|リージョン|asia-northeast1|Function、CloudNATと合わせる|
|IP アドレス範囲|192.168.0.0/24|任意で好きな範囲を指定する、|

### Cloud NAT
Cloud NATの画面から、**NATゲートウェイを作成**を選択します。

|項目名|設定値|備考|
|:---|:---|:---|
|名前|fucnction-connect-nat|任意で好きな名前を！|
|ネットワーク|fucnction-connect-vpc|VPC ネットワークで作ったネットワーク名を指定します|
|リージョン|asia-northeast1|Function、VPCネットワークと合わせる|
|Cloud Router|fucnction-connect-nat-router|任意で好きな名前を指定する|
|NAT マッピング|すべてのサブネットのプライマリとセカンダリの範囲|-|
|NAT IPアドレス|手動|-|
|IPアドレス|IPアドレスを作成する|固定IPにするため(※)|
※固定IPをセット後、KCCS-API側に申請してもらって疎通できるようにする必要があります！



### サーバーレスVPCアクセス (プライベートVPC)
Cloud FunctionsとCloud NATを結ぶためのネットワークコネクタになります。
VPCネットワーク→サーバーレスVPCアクセスからコネクタを作成を選択します。
ここでの注意点は、コネクタのリージョンをFunctionsとCloud NATを合わせてあげる必要があります。
**Functions：asia-northeast1**
**VPC ネットワーク：asia-northeast1-b**
**VPCコネクタ：asia-northeast1**
と、「**asia-northeast1**」に全部寄せて実装しております。
ネットワークは先ほど作成した
**fucnction-connect-vpc**
を指定します。

コネクタの名前は
**fucnction-connect-qiita**
という名前のコネクタにします。

### KCCSAPI(天気情報取得)
KCCSAPIですが、今回のINPUTとなる情報ソースを取得します。
ひと先ずトライアルで申請して、やり取りすれば使えるようになります。
具体的には、以下リンクの中ほどにある、
**データ配信サービストライアルのお申し込みはこちら**
から申し込み頂ければと思います。

https://www.energy-cloud.jp/

### Slack URL作成
今回はIncomingWebhookを利用します。
以下のサイト等の手順を参照して頂いて、URLを払い出してもらい、Functionのパラメータにセットしてリクエストする事で、メッセージの投稿がされるようになります。

https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8


### CloudFuncsionsの作成
では、Function本体の実装に移ります。
GCPのCloudFunctions画面から、「関数の作成」を押下します。
名前を付けて、

HTTPの認証項目は
**認証が必要**
を選んであげます。

ランタイム、ビルド、接続の設定項目は
#### ランタイム環境変数
function-flamework-go
のmain.goで渡していた変数は
で記載していた以下のコードは、Functionsの環境変数にセットします。

```go
        os.Setenv("url", "https://<ログインID>:<パスワード>@rest.energy-cloud.jp/api/v1/weather-forecasts?3h_weathers=1&3h_winds=1&3h_temperatures=1&1d_weathers=1&1d_weathers=1&1d_temperatures=1&latitude=35.6415347691883&longitude=139.741981335114")
        os.Setenv("slack_url", "<Slackで払い出されたURL>")
```

|項目名|設定値|備考|
|:---|:---|:---|
|url|https://<ログインID>:<パスワード>@rest.energy-cloud.jp/api/v1/weather-forecasts?3h_weathers=1&3h_winds=1&3h_temperatures=1&1d_weathers=1&1d_weathers=1&1d_temperatures=1&latitude=35.6415347691883&longitude=139.741981335114|KCCS_APIのURL|
|slack_url|前述のSlack URL作成にて払い出されたURL|-|

#### ネットワーク設定
- プライベートVPC  
Functionsとの紐づけは、Function作成時に「接続」→「VPCコネクタ」にて、前述の
```fucnction-connect-qiita```をセットします。

また、ネットワークの下り設定は、プライベートVPC経由で通信するため、
**すべてのトラフィックを VPC コネクタ経由でルーティングする**
をチェックします。

#### ソースコード
以前の記事で作成したコードを元にして、以下をデプロイします。
https://qiita.com/kccs_api3/items/ac09a4905ba94ae33402

|項目名|設定値|備考|
|:---|:---|:---|
|ランタイム|Go1.13|-|
|エントリポイント|NotifySlack|※Function-Flamework-goのmainから呼び出した、function|

```go
//notify.go
package notify

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

//slack通知用構造体
type Slack struct {
        Text string `json:"text"`
}

//お天気を入れる構造体
type Weather struct {
        Datetime string `json:"datetime"`
        Timezone string `json:"timezone"`
        Area     struct {
                Latitude     float64 `json:"latitude"`
                Longitude    float64 `json:"longitude"`
                Address      string  `json:"address"`
                Weather_info struct {
                        Prefectuqre string `json:"prefecture"`
                        Primary    struct {
                                Code         string `json:"code"`
                                Name         string `json:"name"`
                                Station_name string `json:"station_name"`
                                Station_code string `json:"station_code"`
                        } `json:"primary"`
                } `json:"weather_info"`
        } `json:"area"`
        Weathers3 []struct {
                Time    string `json:"time"`
                Temperature int `json:"temperature"`
                Weather struct {
                        Code string `json:"code"`
                        Name string `json:"name"`
                        Mark string `json:"mark"`
                } `json:"weather"`
                Wind struct {
                        Direction string `json:"direction"`
                        Level string `json:"level"`
                        Description string `json:"description"`
                        Min int `json:"min"`
                        Max int `json:"max"`
                } `json:"wind"`
        } `json:"3h_weathers"`

        Weathers1 []struct {
                Time    string `json:"time"`
                Temperature struct {
                        Lowest int `json:"lowest"`
                        Highest int `json:"highest"`
                } `json:"temperature"`
                Weather struct {
                        Code string `json:"code"`
                        Name string `json:"name"`
                        Mark string `json:"mark"`
                } `json:"weather"`
        } `json:"1d_weathers"`
}

//エントリーポイント
func NotifySlack(w http.ResponseWriter, r *http.Request) {

        api_url := os.Getenv("url")
        slack_url := os.Getenv("slack_url")

        //①KCCS APIサービスを呼び出して天気を取得
        resp, _ := http.Get(api_url)
        defer resp.Body.Close()

        fmt.Println(strconv.Itoa(resp.StatusCode))
        byteArray, _ := ioutil.ReadAll(resp.Body)

           var buf bytes.Buffer
           if e := json.Indent(&buf, byteArray, "", " "); e != nil {
             log.Fatal(e)
           }

           indentJson := buf.String()
           fmt.Println(indentJson)

        //②レスポンスを構造体に格納
        var weather_info Weather
        if e := json.Unmarshal(byteArray, &weather_info); e != nil {
                log.Fatal(e)
        }

        //③お天気通知用にフォーマット変換
        var message string

        message += "気象庁 " + weather_info.Datetime + " 発表の天気予報\n"
        message += "予報地点:" + weather_info.Area.Address + "\n\n"
        message += "1日の天気予報\n"
        for _, w := range weather_info.Weathers1 {
                message += w.Time + "のお天気 : \n" +
                                "　天気: " + w.Weather.Name + "\n" +
                                "　最低気温:" + strconv.Itoa(w.Temperature.Lowest) + "度 \n" +
                                "　最高気温:" + strconv.Itoa(w.Temperature.Highest) + "度 \n\n"
        }

        message += "\n\n\n"
        message += "3時間毎の天気予報\n"

        for _, w := range weather_info.Weathers3 {
                message += w.Time + "のお天気 : \n" +
                                "　天気: " + w.Weather.Name + "\n" +
                                "　気温: " + strconv.Itoa(w.Temperature) + "度\n" +
                                "　風向き: " + w.Wind.Direction + "  " + w.Wind.Description + "\n\n"
        }

        params := Slack{
                Text: message,
        }
        jsonparams, _ := json.Marshal(params)

        args := url.Values{"payload": {string(jsonparams)}}

        //④お天気Slackに通知
        res, err := http.PostForm(slack_url, args)

        if err != nil {
                fmt.Println("Request error:", err)
                return
        }

        defer res.Body.Close()

        _, err = ioutil.ReadAll(res.Body)
        if err != nil {
                fmt.Println("Request error:", err)
                return
        }
}
```

ここまでセットしたら準備完了です！
デバックしたい場合には、
・CloudSchdulerで即時実行
・Functionの「テスト中」タブから「関数をテストする」
を押してあげればデバックできます。

動くのを確認出来たら、毎朝10時を楽しみに待ってください。

こんな感じの通知が届くようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1525729/55008162-e4fe-8def-cd86-590352d64375.png)