今回はGCPのscheduler、Functions、VPC、CloudNATを使って、Slackに天気を通知する機能を実装してみようと思います。
GCPのCloudFunctionsを使ってKCCS APIサービスから取得した天気予報を定期的にSlackに通知する記事の第3弾です。
#各サービスの役割
各サービスの役割は以下とします。
サービス | 役割 |
---|---|
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時を楽しみに待ってください。