GCPのFaaS(Functions-as-a-Service)Cloud Functionsは最近第二世代がリリースされ、(まだbeta期間中ですが)インフラ周りがけっこう強化されました。Cloud StorageとBigQueryにある大規模データを処理するために、HTTPトリガーのアップタイムが60分までに、イベントトリガーも10分まで伸びました。

https://cloud.google.com/functions/docs/2nd-gen/overview

筆者はこの数週間ちょうどCloud Functionsを利用して承認システムを開発しています。Cloud Functions 2nd Genにおけるsecret keyの管理について少し困っていたので、調べたものを共有します。

何が問題か

secret keyなどを明文でCloud Functionsの環境変数に保存するは流石に危ないので、他の方法が必要です。

we recommend that you review the best practices for secret management. Note that there is no Cloud Functions-specific integration with Cloud KMS.

Cloud Functions公式ドキュメントによると、secret keyを管理するベストプラクティスはSecret Managementを利用することです。また、Cloud Functions特有のCloud KMSは存在しません。

https://cloud.google.com/functions/docs/configuring/env-var#managing_secrets

1st genの場合はデプロイする際に-set-secretsというオプションでSecret Management上保存しているキーを追加すれば良いですが、2nd genで同じようなことをやると、-set-secretsはまだ実装されていない(2022年5月)ので、怒られます。

ERROR: (gcloud.beta.functions.deploy) --set-secrets is not yet supported in Cloud Functions V2.

Cloud KMSを使う

Secret Managementはまだ存在しない時代(と言っても3年前)に書かれた記事を読んで、参考になりました。

考え方としては、

  • Cloud KMSで認証情報(文字列)を暗号化する
  • 暗号化した認証情報をCloud Functionsの環境変数に登録する
  • Cloud Functionsの関数内で暗号化された環境変数を復号化する

https://www.apps-gcp.com/kms-with-functions/

KMS ring, KMS keyの設定、キーを暗号化する方法はかなりわかりやすくまとめているので、基本的にこの記事を参考すればいいです。 気をつけるべきポイントは一つだけです。Cloud KMSのSDKがすべにv2にアップグレードされたなので、復号化コードを少し修正する必要があります。

簡単な例を書いたので、共有します。

from google.cloud import kms

PROJECT_ID = os.environ.get("PROJECT_ID")
LOCATION_ID = os.environ.get("LOCATION_ID")
KMS_KEY_RING_ID = os.environ.get("KMS_KEY_RING_ID")
KMS_KEY_ID = os.environ.get("KMS_KEY_ID")

def decrypt_cipher_key(ciphertext: str) -> str:
    client = kms.KeyManagementServiceClient()

    key_name = client.crypto_key_path(
        project=PROJECT_ID,
        location=LOCATION_ID,
        key_ring=KMS_KEY_RING_ID,
        crypto_key=KMS_KEY_ID,
    )

    request = kms.DecryptRequest(
        name=key_name,
        ciphertext=ciphertext,
    )

    response = client.decrypt(request=request)

    return response.plaintext.decode("ascii")

関数decrypt_cipher_key利用すると、base64でエンコードされたキーをデコードできます。

若干面倒になりましたが、Cloud Functions 2nd Genでもsecret keyを安全に管理できるようになりました。Secret ManagementがCloud Functions 2nd Genに対応するまでに一時的な対策としてはまあまあ使えるでしょう。