【Cloud SQL】GolangでDBインスタンスの開始停止をDiscordで通知する

Code

はじまり

リサちゃん
リサちゃん

うーん、あの関数って実行されたのかなあ・・・

135ml
135ml

いちいちコンソールを開くのは面倒だよなあ

以前に作ったCloud SQLを自動で制御するツール

以前にCloud SQLにDBインスタンスを自動で起動もしくは停止するCloud Run Functionsを作成して、自動実行させるようにしました。

いやぁ、ちゃんと自動で実行されているとは思うんですけど、もしかしたらちゃんと実行されてないかもしれないじゃないですか。いや、されてると思うんですけど。しかし、Google Cloudのコンソールをいちいち開くのは面倒くさい。

・・・という心配の種が生まれないように、今回はGoで実装したCloud Run Functionsに、Discordに関数の実行を通知する機能を追加したいと思います。それではやっていきましょう。

DiscordのWebhookを叩きます。

「Discord Go」でググると、こんなパッケージがヒットします。

GitHub - bwmarrin/discordgo: (Golang) Go bindings for Discord
(Golang) Go bindings for Discord. Contribute to bwmarrin/discordgo development by creating an account on GitHub.

しかし今回は、ただWebhookを叩けばいいだけなので、その「discordgo」のパッケージは使わないことにします。

Discord Webhook APIのリファレンスがこちら。

Discord Developer Portal — API Docs for Bots and Developers
Integrate your service with Discord — whether it's a bot or a game or whatever your wildest imagination can come up with...

Discordを開いて、Webhook URLを取得しておきます。

それでは早速Discordに通知する機能の実装部分です。

// Package mypkg contains a Function to request to Discord Webhooks.
package mypkg

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
)

// Payload represents the payload structure for Discord notification.
type Payload struct {
	Username string      `json:"username"`
	Content  string      `json:"content"`
	Embeds   interface{} `json:"embeds,omitempty"`
	TTS      bool        `json:"tts"`
}

// Embed represents a single embed structure.
type Embed struct {
	Title     string `json:"title"`
	Color     int    `json:"color"`
	URL       string `json:"url"`
	Footer    Footer `json:"footer"`
	Timestamp string `json:"timestamp,omitempty"`
}

// Footer represents the footer for an embed.
type Footer struct {
	Text    string `json:"text"`
	IconURL string `json:"icon_url"`
}

// CreatePayloadOfDiscordNotification creates a payload for Discord webhook notification.
func CreatePayloadOfDiscordNotification(botName, content string, embeds []Embed, isTTS bool) (Payload, error) {
	if len(botName) > 2000 {
		return Payload{}, errors.New("'botName' must be less than or equal to 2000 characters")
	}
	if len(content) > 2000 {
		return Payload{}, errors.New("'content' must be less than or equal to 2000 characters")
	}
	if len(embeds) > 10 {
		return Payload{}, errors.New("length of 'embeds' must be less than or equal to 10")
	}

	return Payload{
		Username: botName,
		Content:  content,
		Embeds:   embeds,
		TTS:      isTTS,
	}, nil
}

// CreateEmbedsToNotifyCloudSql creates embeds for notification about DB booting.
func CreateEmbedsToNotifyCloudSql(textToTransit, urlToTransit, iconURL string) []Embed {
	return []Embed{
		{
			Title: textToTransit,
			Color: 4569935,
			URL:   urlToTransit,
			Footer: Footer{
				Text:    "GoogleCloudSQL",
				IconURL: iconURL,
			},
		},
	}
}

// NotifyToDiscord sends a notification to a Discord webhook.
func NotifyToDiscord(webhookURL string, payload Payload) (string, error) {
	payloadBytes, err := json.Marshal(payload)
	if err != nil {
		return "", fmt.Errorf("failed to marshal payload: %v", err)
	}

	req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(payloadBytes))
	if err != nil {
		return "", fmt.Errorf("failed to create HTTP request: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", fmt.Errorf("failed to send HTTP request: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
		return "", fmt.Errorf("unexpected HTTP response: %s", resp.Status)
	}

	return "Notification sent successfully", nil
}

// NotifyDbBooting sends a notification about Google Cloud SQL.
func NotifyDbBooting(content, textToTransit, urlOfConsoleForDbInstance, webhookUrl, iconUrl string) (map[string]string, error) {
	botName := "CloudSQLあゆ"

	embeds := CreateEmbedsToNotifyCloudSql(textToTransit, urlOfConsoleForDbInstance, iconUrl)
	payload, err := CreatePayloadOfDiscordNotification(botName, content, embeds, false)
	if err != nil {
		return nil, err
	}

	response, err := NotifyToDiscord(webhookUrl, payload)
	if err != nil {
		return nil, err
	}

	fmt.Println(response)
	return map[string]string{
		"destinations":    "discord",
		"discord_content": content,
	}, nil
}

こんな風に叩けば動く「NotifyDbBooting」という関数が出来ました。

func main() {
	content := "データベース起動!"
	textToTransit := "DBを起動したよ!"
	urlOfConsoleForDbInstance, ok := os.LookupEnv("MY_DB_INSTANCE_URL"); if !ok {
		fmt.Printf("Error: %v\n", ok)
	}
	webhookURL, ok := os.LookupEnv("MY_DISCORD_WEBHOOK_URL"); if !ok {
		fmt.Printf("Error: %v\n", ok)
	}
	iconURL, ok := os.LookupEnv("CLOUD_SQL_ICON_URL"); if !ok {
		fmt.Printf("Error: %v\n", ok)
	}

	result, err := mypkg.NotifyDbBooting(content, textToTransit, urlOfConsoleForDbInstance, webhookURL, iconURL)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		fmt.Printf("Notification result: %+v\n", result)
	}
}

リクエストでpostするJSONの内容(ペイロード)は、このように構成されています。

  • Username:Webhookで送られる通知の送り主(ボット)の名前。
  • Content:Webhookで送られる通知の文章。
  • Embeds:Webhookで送られる通知では一部の情報を埋め込んで、その情報を目立たせることが出来る。その「埋め込み」の内容。
  • TTS:そのWebhookで送られた通知を表示する時に、その内容を音声として再生させるかどうか。

リクエストでpostするJSON内の"Embeds"フィールドの中身は、このように構成されています。

  • Title:埋込み内のテキストの内容。
  • Color:埋込みの帯の色。
  • URL:埋込み内のテキストにリンクされているURL。
  • Footer
    • Text:フッターのテキストの内容。
    • IconURL:フッターにはアイコンを付けられる。そのアイコン画像のURL。

Cloud Run Functionsの更新。

それでは、「cloudsql-launcher」という名前のCloud Run Fucntionsをデプロイします。既に同じ関数名でデプロイしてあったら上書きされます。

gcloud functions deploy cloudsql-launcher \
  --gen2 \
  --runtime=go123 \
  --project={My_PROJECT} \
  --region={MY_REGION} \
  --source=. \
  --entry-point=ProcessPubSub \
  --trigger-service-account={MY_APP_ENGINE_SERVICE_ACCOUNT} \
  --trigger-topic={MY_PUBSUB_TOPIC_ID} \
  --allow-unauthenticated \
  --timeout=180s \

Cloud Schedulerメッセージの追加。

Pub/Subトピックから決まった時間に発行するCloud Schedulerジョブを新規作成します。

まずは、DBインスタンス起動用のジョブを追加します。

gcloud scheduler jobs create pubsub start-{MY_DB_INSTANCE}-instance \
  --schedule="0 4 * * 0-6" \
  --description="Trigger Cloud Functions to start Cloud SQL instance." \
  --project="{My_PROJECT}" \
  --location="{MY_LOCATION}" \
  --time-zone="Asia/Tokyo" \
  --topic="{MY_PUBSUB_TOPIC_ID}" \
  --message-body="{ \
    \"Instance\": \"{MY_DB_INSTANCE_ID}\", \
    \"Project\": \"{MY_PROJECT_ID}\", \
    \"Action\": \"start\", \
    \"DbInstanceUrl\": \"{MY_DB_INSTANCE_URL}\", \
    \"DiscordWebhookUrl\": \"{MY_DISCORD_WEBHOOK_URL}\", \
    \"CloudSqlIconUrl\": \"{CLOUD_SQL_ICON_URL}\" \
  }" \

まずは、DBインスタンス停止用のジョブを追加します。

gcloud scheduler jobs create pubsub stop-{MY_DB_INSTANCE}-instance \
  --schedule="0 7 * * 0-6" \
  --description="Trigger Cloud Functions to stop Cloud SQL instance." \
  --project="{My_PROJECT}" \
  --location="{MY_LOCATION}" \
  --time-zone="Asia/Tokyo" \
  --topic="{MY_PUBSUB_TOPIC_ID}" \
  --message-body="{ \
    \"Instance\": \"{MY_DB_INSTANCE_ID}\", \
    \"Project\": \"{MY_PROJECT_ID}\", \
    \"Action\": \"stop\", \
    \"DbInstanceUrl\": \"{MY_DB_INSTANCE_URL}\", \
    \"DiscordWebhookUrl\": \"{MY_DISCORD_WEBHOOK_URL}\", \
    \"CloudSqlIconUrl\": \"{CLOUD_SQL_ICON_URL}\" \
  }" \

Cloud Schdulerで発行されるジョブを新しく追加しました。古いものは後で一時停止しておきます。

ちなみに、gcloud scheduler jobs createでジョブを作成した後「[1]+ Done gcloud scheduler jobs create」というメッセージがCLI上で表示されたら、コマンドのどこかが間違っている可能性があります。例えば、JSONの中のURLが間違ってたり。(クエリパラメーターの?(クエスチョンマーク)や&(アンパサンド)をエスケープしていなかったり。でも二重引用符をエスケープしていたらエスケープする必要がなかったり。)

Cloud Run関数の実行結果を確認。

それではデプロイしたCloud Run FunctionsおよびPub/Subの実行結果をDiscordで確認します。

しっかりDBの起動もしくは停止の関数が動くと、関数が起動した時間にDiscordでボットから通知が飛んでいます。

まとめ

今回は、Go言語でCloud SQLに作成したDBインスタンスを自動で開始および停止するCloud Run Functionsが実行された時に、Discord上で通知を飛ばす機能を実装する手順を紹介しました。

これで、Cloud Run Functionsが実行されたことをDiscord上で把握できます。助かるぞお。

おしまい

リサちゃん
リサちゃん

よし、今日も実行されたな

135ml
135ml

DB停止!

以上になります!

コメント

タイトルとURLをコピーしました