2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-09-01

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

構成イメージ

image.png

各サービスの役割

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

サービス 役割
IAM Cloud Functionの認証用サービスアカウント
Cloud Scheduler 処理の起動トリガ 毎朝10:00に起動する
Cloud Functions コントローラ、KCCS APIから気象情報を取得し、Slackに通知する
VPC ネットワーク CloudNATの所属するネットワーク
サーバーレスVPCアクセス
(プライベート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となる情報ソースを取得します。
ひと先ずトライアルで申請して、やり取りすれば使えるようになります。
具体的には、以下リンクの中ほどにある、
データ配信サービストライアルのお申し込みはこちら
から申し込み頂ければと思います。

Slack URL作成

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

CloudFuncsionsの作成

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

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

ランタイム、ビルド、接続の設定項目は

ランタイム環境変数

function-flamework-go
のmain.goで渡していた変数は
で記載していた以下のコードは、Functionsの環境変数にセットします。

        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
//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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1