📊

Cloud Run Functions + SendGrid で Cloud SQLの監視メールを自動送信する

に公開

1. はじめに

クラウドエース安田です。
システム運用においては、Cloud SQL などのリソースの稼働状況や異常をいち早く把握し、迅速に対応することが重要です。そのため、監視結果を定期的にメールで受け取ることで、運用担当者が異常や傾向変化にすぐ気付き、障害の予防や早期対応につなげることができます。

しかし、こうした監視メールを人力で送る運用では、手間やミスが発生しやすく、運用負荷も高まります。こうした課題を解決するために、監視メールの自動化が有効な手段となります。

この記事では、Google Cloud の Cloud Run Functions を使用して Cloud SQL のメトリクスを監視し、SendGrid API を活用してメールを自動送信するシステムの実装方法を紹介します。

この記事の目的

この記事では、以下の目的を達成するための実装方法を紹介します。

  1. Cloud SQL の監視を自動化し、運用負荷を軽減する
  2. 定期的なメール通知により、システムの状態を継続的に把握する
  3. 異常の早期発見と迅速な対応を可能にする
  4. 運用担当者の作業効率を向上させる

これらの目的を達成するために、Cloud Run Functions と SendGrid API を組み合わせた実装方法を詳しく解説します。

システムの概要

実装するシステムは以下のような流れで動作します

  1. Cloud Scheduler が指定された時間にトリガー
  2. Cloud Run Functions が実行され、以下の処理を実施
    • Cloud SQL のメトリクスを取得
    • 現在値とその日の最高値を計算
    • SendGrid API を使用してメールを送信
  3. 受信者は定期的にシステムの状態を確認可能

このシステムにより、Cloud SQL の状態を定期的に把握し、必要に応じて対応することが可能になります。

2. システム構成

使用する Google Cloud サービス

このシステムでは、以下の Google Cloud サービスを使用します

  • Cloud Run Functions

    • Python 3.9 ランタイム
    • HTTP トリガー
    • メトリクス取得とメール送信の処理を実装

https://cloud.google.com/functions?hl=ja

  • Cloud SQL

    • 監視対象のデータベース
    • CPU、メモリ、ディスク使用率のメトリクスを提供

https://cloud.google.com/sql?hl=ja

  • Cloud Scheduler

    • 1 日 3 回(6 時、12 時、18 時)の定期実行
    • HTTP リクエストによる Cloud Run Functions のトリガー
    • 日本時間(Asia/Tokyo)でのスケジュール管理

https://cloud.google.com/scheduler/docs?hl=ja

  • Cloud Monitoring

    • Cloud SQL インスタンスのメトリクス(CPU、メモリ、ディスク使用率など)を取得

https://cloud.google.com/monitoring?hl=ja

  • Secret Manager
    • SendGrid API キーなどのシークレット情報を安全に管理・取得

https://cloud.google.com/security/products/secret-manager?hl=ja

アーキテクチャ図

3. 事前準備

Cloud Run Functions の実行に必要なリソースを作成します。

3.1 必要なリソース

  • Google Cloud プロジェクト
  • サービスアカウント(以下のロールが必要)
    • Cloud Run 起動権限(roles/cloudrun.invoker
    • Cloud Monitoring 閲覧権限(roles/monitoring.viewer
    • Cloud SQL 閲覧権限(roles/cloudsql.viewer
  • Secret Manager に登録した SendGrid API キー
  • Cloud Scheduler の作成
  • Cloud SQL の作成(監視対象)

3.2 サービスアカウントの作成と権限設定

このシステムで使用するサービスアカウントと必要な権限を、Terraform で定義していきます。

service-account.tf
resource "google_service_account" "main" {
  account_id   = "sql-monitoring-sa"
  display_name = "Cloud SQL Monitoring Service Account"
  description  = "Cloud SQLの監視メールを送信するためのサービスアカウント"
}

# 必要な権限の付与
resource "google_project_iam_member" "cloud_functions_invoker" {
  project = var.project_id
  role    = "roles/cloudfunctions.invoker"
  member  = "serviceAccount:${google_service_account.main.email}"
}

resource "google_project_iam_member" "monitoring_viewer" {
  project = var.project_id
  role    = "roles/monitoring.viewer"
  member  = "serviceAccount:${google_service_account.main.email}"
}

resource "google_project_iam_member" "cloudsql_viewer" {
  project = var.project_id
  role    = "roles/cloudsql.viewer"
  member  = "serviceAccount:${google_service_account.main.email}"
}

3.3 Secret Manager の設定

secret-manager.tf
resource "google_secret_manager_secret" "sendgrid_api_key" {
  secret_id = "{your-secret-name}"
  replication {
    automatic = true
  }
}

resource "google_secret_manager_secret_version" "sendgrid_api_key" {
  secret = google_secret_manager_secret.sendgrid_api_key.id
  secret_data = "{your-sendgrid-api-key}"  # SendGrid の API キーを設定
}

3.4 Cloud Scheduler の作成

Cloud Scheduler を作成して、Cloud Run Functions を定期的に実行するように設定します。

cloud-scheduler.tf
resource "google_cloud_scheduler_job" "sql_monitoring" {
  name        = "sql-monitoring-job"
  description = "Cloud SQLの監視メールを送信するジョブ"
  schedule    = "0 6,12,18 * * *"  # 6時、12時、18時に実行
  time_zone   = "Asia/Tokyo"

  http_target {
    http_method = "POST"
    uri         = "<URL>"
    oidc_token {
      service_account_email = google_service_account.main.email
    }
  }
}

3.5 Cloud SQL の作成

監視対象の Cloud SQL インスタンスを作成します。

sql-instances.tf
resource "google_sql_database_instance" "test_instance_1" {
  name             = "test-instance-1"
  database_version = "MYSQL_8_0"
  region           = "asia-northeast1"
  settings {
    tier = "db-f1-micro"
  }
  deletion_protection = false
}

resource "google_sql_database_instance" "test_instance_2" {
  name             = "test-instance-2"
  database_version = "MYSQL_8_0"
  region           = "asia-northeast1"
  settings {
    tier = "db-f1-micro"
  }
  deletion_protection = false
}

3.6 SendGrid の登録と API キーの取得

新規登録

今回は API キーの取得方法のみを紹介します。新規登録の仕方は以下を参考にしてください。
https://sendgrid.kke.co.jp/blog/?p=183

API キーの取得

1.「Create API Key」を選択

ダッシュボードから「Settings > API Keys」を選択して、画面右上の「Create API Key」を選択します。

2.名前とアクセス許可

API キーの名前と、アクセスレベルを選択します。今回は[ Full Access ] で問題ありません。

3.API キーの発行

「Create & View」ボタンを選択すると、API キーが発行され、画面に表示されます。
この API キーは 1 度しか表示されず、再確認することはできないため適切な場所に保存してください。この後の環境変数の設定の際に使用します。

4. Cloud Run Functions のデプロイ

4.1 デプロイ用ソースコード

main.py

Cloud Run Functions で Cloud SQL のメトリクスを取得し、SendGrid 経由で監視メールを送信するメインの実装ファイルです。

main.py
import os
import time
import requests
import functions_framework
from datetime import datetime, timedelta, timezone
from google.cloud import monitoring_v3
from google.protobuf import timestamp_pb2

PROJECT_ID = os.environ['PROJECT_ID']
SENDGRID_API_KEY = os.environ['SENDGRID_API_KEY']
EMAIL_FROM = os.environ['EMAIL_FROM']
EMAIL_TO = os.environ['EMAIL_TO']
CLOUDSQL_INSTANCES = os.environ.get('CLOUDSQL_INSTANCES', '').split(',')
INFRA_PROJECT_ID = os.environ.get('INFRA_PROJECT_ID', PROJECT_ID)

infra_client = monitoring_v3.MetricServiceClient()

def get_metric_value(filter_str, project_id_override=None):
    try:
        target_project = project_id_override if project_id_override else INFRA_PROJECT_ID
        end_time = timestamp_pb2.Timestamp(seconds=int(time.time()))
        start_time = timestamp_pb2.Timestamp(seconds=int(time.time()) - 300)
        interval = monitoring_v3.TimeInterval(start_time=start_time, end_time=end_time)
        series = infra_client.list_time_series(
            request={
                "name": f"projects/{target_project}",
                "filter": filter_str,
                "interval": interval,
                "view": monitoring_v3.ListTimeSeriesRequest.TimeSeriesView.FULL,
            }
        )
        for s in series:
            if s.points: return s.points[0].value.double_value
        return None
    except Exception: return None

def get_daily_peak_metric_value(filter_str, project_id_override=None):
    try:
        target_project = project_id_override if project_id_override else INFRA_PROJECT_ID
        jst = timezone(timedelta(hours=9))
        now_jst = datetime.now(jst)
        midnight_jst = now_jst.replace(hour=0, minute=0, second=0, microsecond=0)
        end_time = timestamp_pb2.Timestamp(seconds=int(now_jst.timestamp()))
        start_time = timestamp_pb2.Timestamp(seconds=int(midnight_jst.timestamp()))
        interval = monitoring_v3.TimeInterval(start_time=start_time, end_time=end_time)
        request_params = {
            "name": f"projects/{target_project}",
            "filter": filter_str,
            "interval": interval,
            "view": monitoring_v3.ListTimeSeriesRequest.TimeSeriesView.FULL,
            "aggregation": {
                "alignment_period": {"seconds": 300},
                "per_series_aligner": monitoring_v3.Aggregation.Aligner.ALIGN_MAX
            }
        }
        series = infra_client.list_time_series(request=request_params)
        max_value, max_time_str = None, None
        for s in series:
            if not s.points: continue
            for point in s.points:
                value = point.value.double_value
                ts_seconds = getattr(point.interval.end_time, 'seconds', None) or \
                             (int(point.interval.end_time.timestamp()) if hasattr(point.interval.end_time, 'timestamp') else None)
                if ts_seconds is None: continue
                point_time_dt = datetime.fromtimestamp(ts_seconds, tz=timezone.utc).astimezone(jst)
                if max_value is None or value > max_value:
                    max_value, max_time_str = value, point_time_dt.strftime('%H:%M')
        return max_value, max_time_str
    except Exception: return None, None

def format_metric_html(label, current_val, peak_val, peak_time_str, current_time_str):
    curr_str = f"{current_val * 100:.2f}%" if current_val is not None else "N/A"
    peak_str = f"{peak_val * 100:.2f}%" if peak_val is not None and peak_time_str else "N/A"
    peak_t_str = peak_time_str if peak_time_str else "N/A"
    return (
        f"<h3>{label}</h3>"
        f"<table style='margin-bottom:10px;border-collapse:collapse;'><tr>"
        f"<td style='padding-right:5px;'>現在値</td><td style='padding-right:5px;text-align:right;'>({current_time_str})</td><td style='text-align:right;'>{curr_str}</td>"
        f"</tr><tr>"
        f"<td style='padding-right:5px;'>今日の最高値</td><td style='padding-right:5px;text-align:right;'>({peak_t_str})</td><td style='text-align:right;'>{peak_str}</td>"
        f"</tr></table>"
    )

@functions_framework.http
def main(request):
    jst = timezone(timedelta(hours=9))
    current_H_M = datetime.now(jst).strftime('%H:%M')
    email_body_parts = []
    metrics_config = [
        {"label": "CPU使用率", "type": "cloudsql.googleapis.com/database/cpu/utilization"},
        {"label": "メモリ使用率", "type": "cloudsql.googleapis.com/database/memory/utilization"},
        {"label": "ディスク使用量", "type": "cloudsql.googleapis.com/database/disk/utilization"}
    ]
    first_instance = True
    for instance_name in filter(None, CLOUDSQL_INSTANCES):
        if not first_instance:
            email_body_parts.append("<hr style='margin:15px 0;border-top:1px solid #ccc;'>")
        first_instance = False
        instance_display_name = f"{INFRA_PROJECT_ID}:{instance_name}"
        email_body_parts.append(f"<h2>{instance_display_name}</h2>")
        for metric in metrics_config:
            metric_filter = f'metric.type = "{metric["type"]}" AND resource.labels.database_id = "{INFRA_PROJECT_ID}:{instance_name}"'
            current_value = get_metric_value(metric_filter)
            peak_value, peak_time = get_daily_peak_metric_value(metric_filter)
            email_body_parts.append(format_metric_html(metric["label"], current_value, peak_value, peak_time, current_H_M))

    subject = "Cloud SQL システム稼働状況"
    html_content = (
        f"<!DOCTYPE html><html><head><meta charset='UTF-8'><title>{subject}</title></head>"
        f"<body style='font-family:sans-serif;margin:15px;'>{''.join(email_body_parts)}</body></html>"
    )
    email_to_list = [email.strip() for email in EMAIL_TO.split(',') if email.strip()]

    response = requests.post(
        "https://api.sendgrid.com/v3/mail/send",
        headers={"Authorization": f"Bearer {SENDGRID_API_KEY}", "Content-Type": "application/json"},
        json={
            "personalizations": [{"to": [{"email": addr} for addr in email_to_list]}],
            "from": {"email": EMAIL_FROM},
            "subject": subject,
            "content": [{"type": "text/html", "value": html_content}]
        }
    )
    response.raise_for_status()
    return "Email sent successfully.", 200

main.py の主な機能と流れ

  1. 初期設定

    スクリプトは実行に必要な API キー、プロジェクト ID、メールアドレス、監視対象の Cloud SQL インスタンス名などを環境変数から読み込みます。これにより、コードを変更せずに設定を管理できます。

  2. メトリクスデータの収集

    Google Cloud Monitoring API を利用して、指定された Cloud SQL インスタンスの主要なパフォーマンスメトリクスを取得します。

  • get_metric_value 関数:CPU 使用率、メモリ使用率、ディスク使用量といったメトリクスの「現在の値」(直近 5 分間)を取得します。
  • get_daily_peak_metric_value 関数:日本標準時(JST)における「今日の最高値」とその発生時刻を、各メトリクスごとに午前 0 時から現在までの範囲で取得します。
  1. HTML メールの生成
  • format_metric_html 関数:収集したメトリクスデータ(現在の値、今日の最高値、発生時刻など)を見やすい HTML 形式のテーブルに整形します。これにより、メール受信者は各メトリクスの状況を一目で把握できます。
  • 監視対象のインスタンスごとに、これらの情報がまとめられ、インスタンスが複数ある場合は区切り線で区切られます。
  1. メール送信
    生成された HTML コンテンツをメール本文とし、SendGrid API を利用して指定されたメールアドレス(複数指定可能)に送信します。件名は「Cloud SQL システム稼働状況」となります。

requirements.txt

このプロジェクトで必要な Python パッケージを定義するファイルです。

requirements.txt
functions-framework>=3.0.0
requests>=2.20.0
google-cloud-monitoring>=2.0.0

env.yaml

Cloud Run Functions の環境変数(メール送信先やインスタンス名など)を設定するファイルです。

env.yaml
EMAIL_FROM: "{your-sender-email@example.com}"
EMAIL_TO: "{recipient1@example.com,recipient2@example.com}"
CLOUDSQL_INSTANCES: "{your-instance-1,your-instance-2}"
INFRA_PROJECT_ID: "{your-cloudsql-project-id}"

4.2 デプロイ手順

1. 作業ディレクトリの準備

# 作業用ディレクトリを作成
mkdir {your-project-name}
cd {your-project-name}

# ソースコードを配置するディレクトリを作成
mkdir src

2. 必要なファイルの配置

ファイルを以下のように配置します

  • src/main.py: メインの Python コード
  • src/requirements.txt: 依存パッケージの定義
  • env.yaml: 環境変数の設定

3. デプロイ用シェル変数の設定

Cloud Run Functions のデプロイ時に必要な各種変数(プロジェクト ID やサービスアカウント、シークレット名など)をシェルで設定する手順です。

# Cloud FunctionをデプロイするプロジェクトID
export PROJECT_ID="{your-project-id}"

# プロジェクト番号を自動取得
export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")

# 関数をデプロイするリージョン
export REGION="{your-region}"

# 関数名
export FUNCTION_NAME="{your-function-name}"

# 関数が使用するサービスアカウントのメールアドレス
export SERVICE_ACCOUNT_EMAIL="{your-service-account@your-project.iam.gserviceaccount.com}"

# Secret Manager に登録した SendGrid API キーのシークレット名
export SECRET_NAME_SENDGRID="{your-secret-name}"

4. Cloud Run Function のデプロイ

ここでは Cloud Run Functions を実際にデプロイするためのコマンド例を示します。

gcloud functions deploy ${FUNCTION_NAME} \
  --region=${REGION} \
  --project=${PROJECT_ID} \
  --runtime=python39 \
  --trigger-http \
  --no-allow-unauthenticated \
  --entry-point=main \
  --source=src/. \
  --env-vars-file=env.yaml \
  --set-secrets="SENDGRID_API_KEY=projects/${PROJECT_NUMBER}/secrets/${SECRET_NAME_SENDGRID}:latest" \
  --memory=256MB \
  --timeout=180s \
  --service-account=${SERVICE_ACCOUNT_EMAIL}

デプロイコマンドのオプション説明

  • --runtime=python39: Python 3.9 ランタイムを使用
  • --trigger-http: HTTP リクエストによるトリガー
  • --no-allow-unauthenticated: 認証が必要
  • --entry-point=main: main.py 内の main 関数をエントリーポイントとして指定
  • --source=src/.: src ディレクトリをソースコードのルートとして指定
  • --env-vars-file=env.yaml: 環境変数の設定ファイル
  • --set-secrets: Secret Manager からシークレットを取得
  • --memory=256MB: メモリ割り当て
  • --timeout=180s: タイムアウト時間(3 分)
  • --service-account: 実行時に使用するサービスアカウント

5. システムの実行

Cloud Run Functions のデプロイが完了すると、Cloud Scheduler が設定したスケジュール(6 時、12 時、18 時)に従って自動的に実行されます。

実行の流れ

  1. Cloud Scheduler が指定された時間に HTTP リクエストを送信
  2. Cloud Run Functions が起動し、以下の処理を実行
    • Cloud SQL インスタンスのメトリクスを取得
    • 現在値とその日の最高値を計算
    • メールを送信

送信されるメールの形式

送信されるメールは以下のような形式になります。

Cloud SQL のシステム稼働状況

・インスタンス: {project-id}:test-instance-1
    ・CPU使用率
        [現在値]       (12:00) 15.23%
        [今日の最高値] (10:30) 45.67%
    ・メモリ使用率
        [現在値]       (12:00) 25.45%
        [今日の最高値] (09:15) 35.89%
    ・ディスク使用量
        [現在値]       (12:00) 45.67%
        [今日の最高値] (11:45) 48.90%

・インスタンス: {project-id}:test-instance-2
    ・CPU使用率
        [現在値]       (12:00) 12.34%
        [今日の最高値] (08:30) 23.45%
    ・メモリ使用率
        [現在値]       (12:00) 34.56%
        [今日の最高値] (10:15) 45.67%
    ・ディスク使用量
        [現在値]       (12:00) 56.78%
        [今日の最高値] (11:30) 58.90%

実際に送られてきたメールの様子

6. まとめ

本記事では、Cloud Run Functions と SendGrid API を組み合わせて、Cloud SQL の監視メールを自動送信するシステムの実装方法を紹介しました。このシステムにより、Cloud SQL の状態を定期的に把握し、必要に応じて対応することが可能になります。

実装のポイントは、メトリクス取得の効率化、運用性の向上にあります。また、今後の改善点として、監視項目の拡充、運用面の強化、セキュリティの強化なども考えられます。

Discussion