Google Cloud Secret Managerを使う

Google Cloud Secret Managerを使う

現在やろうとしていることは、WordPress から Google Cloud の Cloud Run に Amazon ASIN を送り、Python でリアルタイムスクレイピングをし、そのデータを WordPress に返すという WebAPI 連携です。関連の過去記事は次のとおりです。

今回は Cloud Run のサービスで得た Amazon の商品テータを WordPress に安全に返す方法です。

01Google Cloud Secret Manager

データを WordPress に送るには POST 送信すればいいわけですが、安全に送るにはどうしたらいいかです。Google Cloud には Secret Manager というサービスがあり無料枠に入っています。

Secret Manager は、API キー、パスワード、証明書などを保存しておき、アクセス権をもったアカウントだけにアクセスを許すというサービスです。

Secret Manager でシークレットを作成

Secret Manager API を有効にしてシークレットを作成していきます。

ナビゲーションメニュー > セキュリティ > Secret Manager と進みます。

Secret Manager API を有効にします。無料枠には上限がありますので確認の上進んでください。

ページが変わり「シークレットを作成」リンクが現れます。

シークレットの名前と値を入れます。例の値は “this is sample secret key” の短い平文ですが実際にはランダムの長い文字列にします。ローカルに保存したファイルからも読み取れます。

画像にはありませんが、その下のレプリケーションポリシーは自動でいいようです。指定しないと複数課金されるのかと思いましたがそれはないようで単一のロケーションに保存されているとみなされるそうです。下までスクロールしてシークレットを作成します。

サービスアカウントにロールと権限を与える

プロジェクトのサービスアカウントに Secret Manager へアクセスできるロールと権限を与えます。

ナビゲーションメニュー > IAM と管理 > IAM と進み、サービスアカウントの鉛筆アイコンをクリックします。

「別のロールを追加」をクリックし、Secret Manager > シークレットマネージャーのシークレット アクセサー を選択し保存します。

Secret Manager に戻って作成したシークレットの権限を見ますとロールが追加されているはずです。

02Python (FastAPI) Cloud Run の送信側実装

例によって Gemini に聞いてみます。

「Cloud Run で動作する python の fastapi サービスから secret manager を使い外部の php webhook に json データを送る方法を教えてください」

  1. Secret Manager: 外部PHPサーバーのWebhook URLと認証用のAPIトークンをシークレットとして保存
  2. IAM権限: Cloud Runのサービスアカウントに、「Secret Manager のシークレット アクセサー (roles/secretmanager.secretAccessor)」権限を付与
  3. Cloud Run (Python): Secret Managerからシークレットを取得し、HTTPヘッダーにトークンを含めてPHPサーバーへPOST送信
  4. PHP Webhook: 受け取ったリクエストヘッダーのトークンを検証し、処理を行う

1 と 2 は済んでいますので 3 の python のコードです。Gemini が教えてくれたコードを若干変更して試してみます。

import os
import json
import httpx
from fastapi import FastAPI, HTTPException
from google.cloud import secretmanager
from pydantic import BaseModel

app = FastAPI()

# GCS Secret Managerからシークレットを取得する関数
def get_secret(secret_id: str, version_id: str = "latest") -> str:
    client = secretmanager.SecretManagerServiceClient()
    # Cloud Runでは環境変数GOOGLE_CLOUD_PROJECTが設定されていないのでデプロイ時に次のオプションをつける
    # --set-env-vars GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
    project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") 
    name = f"projects/{project_id}/secrets/{secret_id}/versions/{version_id}"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("UTF-8")

# 送信するデータ構造
class WebhookData(BaseModel):
    user_id: int
    message: str

@app.post("/")
async def read_root(payload: WebhookData):
    try:
        # Geminiはwebhook_urlもSecret Managerを使うよう指示している
        webhook_url = "https://YOUR-PHP-WEBHOOK-URL"

        # Secret ManagerからAPIキーを取得
        api_key = get_secret("WEBHOOK-API-KEY") # 作成したシークレット名

        # HTTPヘッダーの設定
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {api_key}"
        }
        
        # 外部PHPへJSONをPOST送信
        async with httpx.AsyncClient() as client:
            response = await client.post(
                webhook_url,
                json=payload.dict(),
                headers=headers,
                timeout=10.0
            )
            response.raise_for_status() # エラーチェック

        return {"status": "success", "message": "Webhook sent"}

    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

ポイントは Secret Manager にアクセスしてシークレットを取り出す関数 get_secret です。

結構苦労したのは Cloud Run は現在のプロジェクト名を環境変数に持っていませんのでデプロイ時にオプションで指定する必要があります。プロジェクト名の直書きで対応すればいいのですがそれですと汎用性がなくなります。

03PHP Webhook の受け側実装

続いて受け側の PHP のコードです。とりあえずはチェック用の簡単なコードで受けてみます。

チェック用ですのでシークレットキーをコード内に書いています。運用の際は環境変数を使うか Google Auth Library を使って取得することになります。これは次回です。

<?php
// 認証キーのチェック
// 本番では環境変数やGoogle Auth Libraryを使用
$headers = getallheaders();
$expected_key = "this is sample secret key"; // シークレットの値

if (!isset($headers['Authorization']) || $headers['Authorization'] !== 'Bearer ' . $expected_key) {
    header('HTTP/1.1 401 Unauthorized');
    exit('Unauthorized');
}

// JSONデータの受信
$json = file_get_contents('php://input');
$data = json_decode($json, true);

if ($data) {
    // 処理ロジック (データベースに保存など)
    file_put_contents('log.txt', print_r($data, true), FILE_APPEND); // ログ保存
    
    header('Content-Type: application/json');
    echo json_encode(["result" => "received", "data" => $data]);
} else {
    header('HTTP/1.1 400 Bad Request');
    echo json_encode(["error" => "Invalid JSON"]);
}

受信チェック

受信側でも結構苦労しました。Authorazation を受信しないのです。

理由は、 Authorization ヘッダの値が環境変数 HTTP_AUTHORIZATION に入っていないでした。実は受信のチェックを WordPress の外でやっていましたので WordPress の .htaccess が効いていませんでした。通常 WordPress ではデフォルトで public_html に次の .htaccess が設定されます。

RewriteEngine On
RewriteRule .* – [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index.php$ – [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

2行目が Authorization ヘッダの値を環境変数 HTTP_AUTHORIZATION に入れる設定です。ですのでチェック用のディレクトリの .htaccess に

RewriteEngine On
RewriteRule .* – [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

を入れておく必要があったということです。

認証を外して curl でチェックしてみます。

$ curl -X POST https://test-app-695878881525.asia-northeast1.run.app -H "Content-Type: application/json" -d '{"user_id": 1234567890, "message": "Hello"}'
{"status":"success","message":"Webhook sent"}

即、レスポンスが返ってきます。php のログファイル log.txt をみてみます。

Array
(
    [user_id] => 1234567890
    [message] => Hello
)

問題ありませんね。

実際にいろいろ試してやってみる時間よりも記事を書くのに時間がかかります(笑)。

次は php から Google Auth Library を使って Secret Manager にアクセスする方法です。