GCE上にfunctions-framework-goをインストールして、お天気をSlack通知してみよう

##概要
CloudFunctionの開発環境、functions-framework-goを使って、KCCS APIサービスから取った天気予報をSlackに通知してみたいと思います。

最終的には、CloudFunctionのを使って定期的に天気を通知するbotを作ってみます。

その中で使用する、functions-framework-go、KCCS APIサービス、CloudFunctionの解説もしていければなと思います。

KCCSは、GCPのパートナー企業としてGCPを使っており、せっかくなのでこちらのクラウド環境を利用して開発を行いたいと思います。
※GCP=GoogleCloud Platformのこと
※GCE=Google Compute Engineのこと

https://www.kccs.co.jp/ict/service/gcp/

##functions-framework-goとは
コンソール上でCloudFunctionのコーディングができるようになっており、Function開発に伴う、デプロイ、デバック時間を短縮することができる便利なツールです。

※Functionにいきなりデプロイしようとすると、デプロイ待ち時間、コンパイルエラー、実行時エラーが発生して時間がかかりますので、こちらを使うと便利です。

##KCCS-APIとは
KCCS APIデータ配信サービスは、気象庁が発表する気象・天気予報データをWebサイトやシステム・アプリ開発で利用しやすいAPI形式で配信するサービスです。
気象庁の専門的なデータフォーマットを利用しやすいように加工、ピンポイント地点の気象データを欲しい時に欲しい分だけ取得でき、月額3,000円からの従量課金制で安価に利用できます。
気象予報を基にしたAIによる発電・充電・消費電力予測制御、Webサイトやアプリ、デジタルサイネージなどへの気象情報の表示等、幅広い分野でご利用いただけます。

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


全3回で投稿したいと考えており以下になる予定です。


##1.functions-framework-goを使って、KCCS APIサービスからお天気を取得してSlack通知してみよう
仕様
>・KCCS APIサービスを利用してHTTP通信でお天気取得
>・お天気のJSONを構造体に入れて整形
>・SlackにIncoming Webhookで通知
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1198009/c9d7d179-03fd-2613-b829-38179c57218f.png)


##2.KCCS APIサービス 天気予報データ配信機能(weather-forecasts)について
今回使ったAPIについて解説します。

ドキュメントはKCCS APIサービスのホームページの、データ配信サービス仕様書 2.3.天気予報データ配信機能（REST API）。


##3.Google Cloud Functionsを使って、定期的に(KCCS APIサービスからお天気をSlackに通知してみよう
Cloud Functionにデプロイして、Cloud Schedulerをトリガに定期的にお天気を通知してみよう。

最終構成
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1198009/393958c2-4d01-f2e8-3c50-bd65c52fb8ea.png)
※GCE経由となるのは、KCCS-APIが固定IPによる認証を行っているため参照可能なIPを経由する必要があるためです。  
<br>
<br>



まずは、**第1回目**
##GCE上にfunctions-framework-goをインストールして、お天気をSlack通知してみよう


functions-framework-goを使います。

Golangが入った環境でfunctions-framework-goをインストールして、動作確認をする所から始めたいと思います。

以下のサイトより

https://cloud.google.com/functions/docs/running/function-frameworks?hl=ja#functions-local-ff-install-go

```
$ go get github.com/GoogleCloudPlatform/functions-framework-go/funcframework
```


※執筆時はgo1.15環境を利用しておりました。
　go1.16は→「go install github.com/GoogleCloudPlatform/functions-framework-go/funcframework」
　となり、Googleのサイト上では最新に合わせて記載されているようです。


https://future-architect.github.io/articles/20210209/

```
#go path に移動して作業ディレクトリを作ります。
 
$ mkdir ./work
$ mkdir ./work/notify_slack
 
$ cd /work/notify_slack
 
#goの初期化(※Functionに記載する、go.modができます。)
 
$ go mod init kccs.co.jp
```

コードを書きます。
functionの代わりとなるイベントハンドラをコーディングします。
functions-framework-goは、イベントを受け取ると、ここでセットしたイベントハンドラを呼ぶようにします。
Slackへの通知は「Incoming Webhook」を使います。


処理の流れとしては、
>①KCCS APIサービスを呼び出して天気を取得
>②レスポンスを構造体に格納
>③お天気通知用にフォーマット変換
>④お天気Slackに通知

となります。

```
$ vi main.go
```

```go
//main.go
package main
 
import (
        "log"
        "os"
        "context"
        "github.com/GoogleCloudPlatform/functions-framework-go/funcframework"
        "kccs.co.jp/notify"
)
 
func main() {
 
        //ランタイム環境変数
        //latitude=35.6415347691883&longitude=139.741981335114 はKCCSの三田事業所の緯度・経度です。
        //slack_urlについては、Slackで取得したURLを使ってください。
        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>")
 
        ctx := context.Background()
        if err := funcframework.RegisterHTTPFunctionContext(ctx, "/", notify.NotifySlack); err != nil {
                log.Fatalf("funcframework.RegisterHTTPFunctionContext: %v\n", err)
        }
        // Use PORT environment variable, or default to 8080.  
        port := "8080"
        if envPort := os.Getenv("PORT"); envPort != "" {
                port = envPort
        }
        if err := funcframework.Start(port); err != nil {
                log.Fatalf("funcframework.Start: %v\n", err)
        }
}

```


KCCS APIサービス呼び出し部分

```
$ mkdir notify/
$ vi notify/notify.go
```


``` 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
        }
 
}
```


```
#コンパイルできるか確認
$ go build 
 
#function 起動
$ go run main.go &
```
※2重起動できないので、止めたい時はkill %1してください。


function呼び出し
起動

```
$ curl localhost:8080
```

通知がきました！
![image2021-6-15_15-43-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1198009/99fdb8bd-337a-c2e6-730f-abb3307d3b94.png)

天気情報がちゃんと取れてますね。
これを定期的にサーバレスに通知できるように、次回以降デプロイしていきたいと思います。