Cloud SQL の止め忘れを Terraform テンプレートで対策する

Cloud SQL の止め忘れを Terraform テンプレートで対策する

2025.11.29

こんにちは、すらぼです。

皆さんは Cloud SQL を検証用に立ち上げて、そのまま消し忘れたことはありますか?私はあります。
色々な検証を行う中でインスタンスを立ち上げては止めて...を繰り返すうちに止め忘れ、翌朝の料金アラートを見てげんなりする。そんなことを何度か繰り返してきました。

流石に人間の注意力でこれに立ち向かうのは限界と感じたので、仕組み化して対策を行おうと思います。

方針を決める

「止め忘れを防止する」という観点から、以下の方針で作ってみます。

  • 止める操作に特化し、起動処理は行わない
    • プロジェクト内インスタンスを全て停止する
    • SQL インスタンスに auto_stop: false ラベルがある場合は停止しない
  • Terraform テンプレートで使いまわしやすいように

特に基本動作を「止める」にすることで、うっかりのリスクを極限まで減らすようにしています。これが例えば「ラベルがついたインスタンスだけ止める」だと、ラベルをつけ忘れ無いようにするという別の課題が発生してしまいます。

ただ、問答無用で全部止めるのは使いにくいので、特定のラベルがあるインスタンスはスキップできる処理も入れるようにしてみます。

出来上がったもの

先に、出来上がったテンプレートのリポジトリを紹介します。
デプロイも簡単にできるように「Open in Cloud Shell」も作ってみたので、紹介します。

https://github.com/Gre212/cloud-sql-stopper

デプロイの方法

1. 「Open in Cloud Shell」からリポジトリを開く

上記の GitHub リポジトリの README.md にある「Open in Cloud Shell」 ボタンをクリックします。

CleanShot 2025-11-29 at 21.21.12.png

Cloud Shell が開き、以下の承認画面が表示されます。「リポジトリを信頼する」にチェックを入れて「確認」と進みます。

CleanShot 2025-11-29 at 21.20.43.png

すると、Cloud Shell にリポジトリがクローンし、作業ディレクトリが表示されます。
また、以下のように Cloud Shell に加えて「チュートリアル」の画面が表示されます。これ以降は、「チュートリアル」に表示される手順に沿って進んでいきます。

CleanShot 2025-11-29 at 21.23.22.png

2. プロジェクトの選択

では、まずプロジェクトの選択を行います。
「プロジェクトを選択」ボタンから、今回自動停止スクリプトをデプロイするプロジェクトを選択します。

注意: 本番環境や、本番用 Cloud SQL インスタンスが含まれるプロジェクトには絶対にデプロイしないでください。これ以降の手順を実施すると、プロジェクト内のすべてのDBが停止される処理が設定されます。

CleanShot 2025-11-29 at 21.27.33.png

プロジェクトを選択したら、「開始」をクリックします。

改めて、プロジェクトが正しいことを確認してください。正しければ「次へ」、誤っている場合は「前へ」でプロジェクトを選択しなおします。

CleanShot 2025-11-29 at 21.43.17.png

3. デプロイ作業

次に、Terraform を初期化するために terraform init を実行します。
ちなみに、コマンドは Cloud Shell アイコン(スクリーンショット矢印部分)をクリックすることで、ターミナルに自動で入力されます。

CleanShot 2025-11-29 at 21.45.04.png

初期化が完了したら、今度はデプロイです。
同じように Cloud Shell アイコンをクリックして、 terraform apply を実行します。

CleanShot 2025-11-29 at 21.47.30.png

確認プロンプトが表示されたら、 yes を入力してあとは待ちます。

完了すると、以下のような緑色のメッセージと関数URLが表示されます。これで完了です。

CleanShot 2025-11-29 at 21.54.28.png

4. 動作確認

では、最後に動作確認として、関数を呼び出して実際にSQLインスタンスを止めてみます。
停止のテストになるので、事前にSQLインスタンスを起動しておきます。

Cloud Run functions に stop-cloudsql-instances という関数ができているはずです。
関数の画面上の「テスト」から「Cloud Shell でテストする」を選択します。

CleanShot 2025-11-29 at 21.57.44.png

ペイロードはデフォルトのままで問題ありません

CleanShot 2025-11-29 at 22.02.50.png

そして、成功すると以下のようなログが表示されます。

CleanShot 2025-11-29 at 22.15.51.png

このログでは、計1個のインスタンスを処理し、1台の停止処理があったことが確認できます。

Processed 1 instances: Stopped: 1, Skipped: 0, Errors: 0

動作確認も済み、これで22時になったらインスタンスが自動で止まるようになりました!

中身の紹介

成果物の紹介も済んだところで、何をやっているかを軽く紹介します。

今回作るのは、以下の通りです。

  • Cloud Run functions : 止め忘れの処理の実行部分
  • Cloud Scheduler : 関数を定期実行するためのトリガー
  • その他周辺リソース:
    • 各種サービスアカウント、カスタムロール

Cloud Scheduler に関しては特段紹介することも無いため、今回は Cloud Run functions に絞って紹介します。

Cloud Run functions の中身

関数のコードは以下のようになっています。(長いので閉じています)

main.py
import logging
import google.auth
from googleapiclient import discovery
from googleapiclient.errors import HttpError

# Set up logging once at module level
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def stop_cloud_sql_instances(request):
    """Stops Cloud SQL instances that do not have the 'auto_stop' label set to 'false'."""

    try:
        # Get credentials and project ID
        credentials, project_id = google.auth.default()

        # Build the Cloud SQL Admin API service
        service = discovery.build('sqladmin', 'v1beta4', credentials=credentials)

        # List all instances in the project
        list_request = service.instances().list(project=project_id)
        response = list_request.execute()
        instances = response.get('items', [])

        if not instances:
            logger.info("No Cloud SQL instances found.")
            return "No instances found", 200

        stopped_count = 0

        for instance in instances:
            name = instance.get('name')
            state = instance.get('state')
            user_labels = instance.get('settings', {}).get('userLabels', {})

            # Check if instance should be skipped
            if user_labels.get('auto_stop') == 'false':
                logger.info(f"Skipping instance {name} (auto_stop=false)")
                continue

            # Get current activation policy
            activation_policy = instance.get('settings', {}).get('activationPolicy', 'ALWAYS')

            # Skip if instance is already stopped
            if state in ['STOPPED', 'SUSPENDED'] or activation_policy == 'NEVER':
                logger.info(f"Instance {name} is already stopped (State: {state}, ActivationPolicy: {activation_policy})")
                continue

            # Skip if instance is not runnable
            if state != 'RUNNABLE':
                logger.info(f"Instance {name} is in state {state} with ActivationPolicy {activation_policy}")
                continue

            # Stop the running instance
            logger.info(f"Stopping instance {name} (State: {state}, ActivationPolicy: {activation_policy})...")

            try:
                stop_request_body = {
                    "settings": {
                        "activationPolicy": "NEVER"
                    }
                }

                stop_op = service.instances().patch(
                    project=project_id,
                    instance=name,
                    body=stop_request_body
                ).execute()

                logger.info(f"Stop operation initiated for {name}: {stop_op.get('name')}")
                stopped_count += 1

            except Exception as patch_error:
                logger.error(f"Unexpected error stopping instance {name}: {patch_error}")
                # Continue processing other instances even if one fails

        return f"Processed {len(instances)} instances. Initiated stop for {stopped_count} instances.", 200

    except HttpError as e:
        logger.error(f"HTTP API error occurred: {e}")
        return f"HTTP API error: {e}", 500
    except Exception as e:
        logger.error(f"Unexpected error occurred: {e}")
        return f"Unexpected error: {e}", 500

簡単に要点を解説します。

まず、以下の部分でプロジェクト内のすべてのインスタンスを取得しています。

    # List all instances in the project
    list_request = service.instances().list(project=project_id)

次に以下の部分でラベルを確認し、 auto_stop: false があるインスタンスは処理をスキップしています。

    # Check if instance should be skipped
    if user_labels.get('auto_stop') == 'false':
        logger.info(f"Skipping instance {name} (auto_stop=false)")
        continue

また、以下の部分では「起動中」以外のインスタンスの処理をスキップするようにしています。
Cloud SQL API では、起動中以外のインスタンスに停止処理を行うとエラーが出るため、スキップ処理が必要です。

    # Skip if instance is not runnable or already set to stop
    if state != 'RUNNABLE' or activation_policy == 'NEVER':
        logger.info(f"Instance {name} is not stoppable (State: {state}, ActivationPolicy: {activation_policy})")
        continue

あとはこれを全インスタンスループさせて終了です。シンプルですね。

終わりに

簡単ですが、今回の紹介は以上になります。

今回の対応で、布団に入った後に「あっ、、消し忘れてるな、、、」と、いそいそPCに戻りインスタンスを止めるようなことをしなくて良くなりました。明日からはよく眠れそうです。

また、「Cloud Shell で開く」コマンドは今回初めて触りましたが、思ったよりも便利で感動しました。
まだ使い込めていませんが、Cloud Shell に不慣れな方でもスムーズにコマンドを叩ける点は非常に便利でした。プロジェクト選択もマネージドの機能が使えるのも嬉しいポイントと感じました。

少し話が脱線しましたが、この記事が同じような悩みを持つ方の助けになれば幸いです。
以上、すらぼでした。

この記事をシェアする

FacebookHatena blogX

関連記事