はじまり

1日で250円・・・?!

1年で91250円・・・!?
なんだこの金額はァァ
以前にCloud SQLにDBインスタンスを作成して数日ぐらい泳がせておきました。
そして、月初に先月の請求額が届いたので、確認すると・・・。

えっっっ。高すぎない

Cloud SQLの金額が嵩んだ原因
この記事を参考に、Cloud SQLの費用を抑える手段を3つ挙げられました。
DBのインスタンスは基本的に「常にオン」にするか、オフにしたい時に「オフ」にするかしかないようです。まあ実際にDBインスタンスはずっと稼働している状態なんですよね。

そして、Cloud SQLを使うためにGoogle Compute EngineのAPIを有効化したので、確かに先程のような膨大な金額になることは想像が付くことでしたか・・・。
これは間違いなくテコ入れしなければ。
Cloud SQLインスタンスを開始および停止させる
そこで今回は、Cloud SQLのDBインスタンスを開始および停止させるCloud Run Functionsを作成して、その関数をPub/SubおよびCloud Schedularでスケジューリング実行させていこうと思います。
このGoogle Cloud公式のガイドを参考にしています。

DBのインスタンスを作成する。
これは以前の記事で行ったのでそちらを参照。
先程見せたようなこんな感じのDBインスタンスが作成されていればOKです。今回はDBMエンジンはPostgreSQLです。

Cloud SQLインスタンスを開始停止できる権限を設定する。
次に、IAM上でCloud SQLインスタンスを開始停止できる権限を設定します。
Google Cloud コンソールの IAM セクションに移動し、Cloud Functions で使用される「App Engine デフォルト サービスアカウント」を見つけます。このサービスアカウントのサフィックスは「@appspot.gserviceaccount.com」です。鉛筆アイコンをクリックして編集します。

「権限の編集」ダイアログ ウィンドウで 「別のロールを追加」ボタンをクリックします。追加する「Cloud SQL 管理者」ロールを選択し、「保存」ボタンをクリックします。

IAMを設定したら、サービスアカウントのアドレス(プリンシパル)をメモっておきます。
これから作る関数をデプロイする際に使います。
Pub/Subのトピックを作成する。
次に、Cloud Pub/Subのトピックを作成します。「トピックを作成」をクリックします。

今回のトピックIDは「DbInstanceMgmt」としておきます。トピックを作成したら、トピックIDをメモっておきます。
これから作る関数をデプロイする際に使います。

関数を作成する。
それでは早速、関数を作成していきます。言語はGoです。
// Package p contains a Pub/Sub Cloud Function.
package p
import (
"context"
"encoding/json"
"log"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
)
// PubSubMessage is the payload of a Pub/Sub event.
// See the documentation for more details:
// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
type PubSubMessage struct {
Data []byte `json:"data"`
}
type MessagePayload struct {
Instance string
Project string
Action string
}
// ProcessPubSub consumes and processes a Pub/Sub message.
func ProcessPubSub(ctx context.Context, m PubSubMessage) error {
var psData MessagePayload
err := json.Unmarshal(m.Data, &psData)
if err != nil {
log.Println(err)
}
log.Printf("Request received for Cloud SQL instance %s action: %s, %s", psData.Action, psData.Instance, psData.Project)
// Create an http.Client that uses Application Default Credentials.
hc, err := google.DefaultClient(ctx, sqladmin.CloudPlatformScope)
if err != nil {
return err
}
// Create the Google Cloud SQL service.
service, err := sqladmin.NewService(ctx, option.WithHTTPClient(hc))
if err != nil {
return err
}
// Get the requested start or stop Action.
action := "UNDEFINED"
switch psData.Action {
case "start":
action = "ALWAYS"
case "stop":
action = "NEVER"
default:
log.Fatal("No valid action provided.")
}
// See more examples at:
// https://cloud.google.com/sql/docs/sqlserver/admin-api/rest/v1beta4/instances/patch
rb := &sqladmin.DatabaseInstance{
Settings: &sqladmin.Settings{
ActivationPolicy: action,
},
}
resp, err := service.Instances.Patch(psData.Project, psData.Instance, rb).Context(ctx).Do()
if err != nil {
log.Fatal(err)
}
log.Printf("%#v\n", resp)
return nil
}
ちなみに、上記のコードは参照した公式のガイドと1行だけ違います。以下の行です。sqladmin.New
はdeprecatedでした。
// Before
service, err := sqladmin.New(hc)
// After
service, err := sqladmin.NewService(ctx, option.WithHTTPClient(hc))
gcloud
でデプロイする場合はこう書きます。(GUIでやる場合は公式のガイドを参照。)
gcloud functions deploy cloudsql-launcher \
--gen2 \
--runtime=go123 \
--region={MY_REGION} \
--source=. \
--entry-point=ProcessPubSub \
--trigger-topic=DbInstanceMgmt \
--allow-unauthenticated \
--timeout=180s \
初めて「--trigger-topic
」の引数を指定してデプロイするとこのようなメッセージが表示されるかもしれません。その時は「y」を入力しておきます。
API [eventarc.googleapis.com] not enabled on project [<your-project-id>]. Would you like to enable and retry (this will take a few minutes)? (y/N)?
このデプロイするときのサービスアカウントが悩みどころです。なぜなら、サービスアカウントを紐づけられそうなフィールドというか引数が何種類もあるからです。
- –service-account・・・実行時に関数に関連付けられている IAM サービス アカウントの電子メール アドレス。サービス アカウントは、実行中の関数の ID を表し、関数が持つ権限を決定します。 指定しない場合は、関数はプロジェクトのデフォルトのサービス アカウントを使用します。
- –run-service-account・・・関数の Cloud Run サービスに関連付けられている IAM サービス アカウントの電子メール アドレス。サービス アカウントは、実行中の関数の ID を表し、関数が持つ権限を決定します。 指定しない場合は、関数は Compute Engine のプロジェクトのデフォルトのサービス アカウントを使用します。
- –trigger-service-account・・・関数の Eventarc トリガーに関連付けられている IAM サービス アカウントの電子メール アドレス。これは、認証された呼び出し に使用されます。 指定しない場合は、関数は Compute Engine のプロジェクトのデフォルトのサービス アカウントを使用します。
- –build-service-account・・・ビルドステップで使用される資格情報となる、IAM サービス アカウント。以下の形式で指定する必要があります。
projects/${PROJECT_ID}/serviceAccounts/${ACCOUNT_EMAIL_ADDRESS}
指定しない場合は、関数は Cloud Build のプロジェクトのデフォルトのサービス アカウントを使用します。
今回は、「--trigger-service-account
」を指定することで後続の作業を進めることが出来ました。デプロイした関数が、先程設定したトピックIDとサービスアカウントに紐付いている状態で反映されていることが確認できます。

ちなみにgcloudでCloud Run Functionsをデプロイする際の引数の一覧は、この公式リファレンスで確認することが出来ます。(Cloud Run Functionsは、「今後数か月で、Cloud Functions を Cloud Run UI に統合する予定です。」らしいので、もしかしたらこのサービスアカウントを記入する引数が変わるかもしれません。)

Cloud Functionsの関数にPub/Subの権限を追加する
関数を作成してコンソール上で確認すると、「Eventarcトリガー」のところにこのような注意書きがされていました。

「このプロジェクトのサービスアカウントにロールroles/iam.serviceAccountTokenCreatorが付与されている必要があります。これは後で変更できます。」と記載されていますので、付与しておきます。
Cloud Functions の関数が想定どおりに動作することを確認する
それでは関数の挙動を確認します。
Google Cloudのコンソールの Pub/Sub セクションに移動し、「DbInstanceMgmt」トピックを選択します。「メッセージをパブリッシュ」ボタンをクリックします。

そしたら、次の JSON メッセージを貼り付けて、メッセージを公開します。(<your-db-instance-id>
は実際のDBインスタンス名、<your-project-id>
は実際のプロジェクト ID に置き換えます)
{
"Instance": "<your-db-instance-id>",
"Project": "<your-project-id>",
"Action": "stop"
}
関数の実行をしばらく待ってからDBインスタンスを見ると、停止していることが確認できました。

次に、別の Pub/Sub メッセージをパブリッシュしてインスタンスを起動します。
Cloud Console の Pub/Sub セクションに移動し、「InstanceMgmt」トピックを選択します。[メッセージのパブリッシュ] ボタンをクリックし、次の JSON メッセージを貼り付けます。今回はアクションを「start」にします。(<your-db-instance-id>
は実際のDBインスタンス名、<your-project-id>
は実際のプロジェクト ID に置き換えます)。
{
"Instance": "<your-db-instance-id>",
"Project": "<your-project-id>",
"Action": "start"
}
関数の実行をしばらく待ってからDBインスタンスを見ると、再び開始していることが確認できました。

Cloud Scheduler ジョブを作成する
Cloud Functions の関数が想定通りに動作することが確認できたので、最後のステップとして、インスタンスを自動的に起動して停止する 2 つの Cloud Scheduler ジョブを作成します。
Cloud Console のCloud Scheduler セクションに移動し、[ジョブのスケジュール設定] ボタンをクリックします。Cloud Schedular APIが有効になっている必要があります。

「start-db-instance」という指定したCloud SQLインスタンスを開始するためのジョブを、スケジュール実行できるように作成します。
ジョブを実行するタイミングを指定する「頻度」には「0 4 * * 0-6」と入力します。これで、ジョブが日曜日から土曜日の毎日午前4時に実行されるようになります。

ターゲットタイプに「Pub/Sub」、そして先程設定したトピックIDを設定して、先程試したDBインスタンス開始用のメッセージと同じものをメッセージ本文に設定します。

そしたら次に、「stop-db-instance」という指定したCloud SQLインスタンスを停止するためのジョブを、スケジュール実行できるように作成します。
ジョブを実行するタイミングを指定する「頻度」には「0 7 * * 0-6」と入力します。これで、ジョブが日曜日から土曜日の毎日午前7時に実行されるようになります。

ターゲットタイプに「Pub/Sub」、そして先程設定したトピックIDを設定して、先程試したDBインスタンス停止用のメッセージと同じものをメッセージ本文に設定します。

以上の設定が反映されると、2つのジョブスケジュールが作成されました。

ちなみに、上記の設定で作成したジョブスケジュールと同じものを、gcloud
からでも作成することが出来ます。
こちらは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." \
--location="{MY_LOCATION}" \
--time-zone="Asia/Tokyo" \
--topic=DbInstanceMgmt \
--message-body="{ \
"Instance": "{MY_DB_INSTANCE_ID}", \
"Project": "{MY_PROJECT_ID}", \
"Action": "start" \
}" \
こちらはDBインスタンス停止用のジョブスケジュール。
gcloud scheduler jobs create pubsub stop-{MY_DB_INSTANCE}-instance-02 \
--schedule="0 7 * * 0-6" \
--description="Trigger Cloud Functions to stop Cloud SQL instance." \
--location="{MY_LOCATION}" \
--time-zone="Asia/Tokyo" \
--topic=DbInstanceMgmt \
--message-body="{ \
"Instance": "{MY_DB_INSTANCE_ID}", \
"Project": "{MY_PROJECT_ID}", \
"Action": "stop" \
}" \
同様に作成できました。

これでCloud SQLを節約できるはず・・・。
後日、Cloud SQLを節約できているかどうかを確認しました。
おおっ、使用料が減っています! 1日あたり50円ぐらいに収まりました。(しかし、1日3時間だけの起動でも18,250円か・・・。けっこう掛かるもんなんだな・・・。)

まとめ
今回は、Go言語でCloud SQLに作成したDBインスタンスを自動で開始および停止するCloud Run Functionsを実装する手順を紹介しました。
以下、まとめです。
- Cloud SQLのDBインスタンスは、基本的にはデフォルトでずーっと稼働している状態になってしまう。なので使用料金が嵩む。
- Cloud Run FunctionsでCloud SQLの稼働の開始および停止を制御できる。
- さらに、Cloud SchedularおよびCloud Pub/Subで、Cloud Run Functionsを自動実行できる。
- Cloud Run Functionsの制御には、「App Engine default service account」に、「Cloud SQL 管理者」ロールを付与する必要がある。
- Cloud FunctionsがCloud Run UI に統合されたら、また何かしらの別の設定が必要になるかもしれない。(2024-12-02時点では不明。)
これで年に91,250円も払う必要が無くなると思います。早めに気付いて良かったぁぁ。
おしまい

怖かったぁぁ

ちょっと使うからクラウドは安くなるんだよな。
以上になります!
コメント