🐟

Firebase Functionsでクレデンシャルを扱う

2023/03/30に公開

DIMBULA では、SlackアプリやGithubアプリと連携するためのクレデンシャル情報がありますが、Firebase Functionsのプログラムにハードコードせずに、またリポジトリにも含まず、どう安全に扱うことができるか紹介します。

GCP Secret Manager

クレデンシャル等のシークレット情報を含むプログラムをFirebase Functionsにデプロイすると、それらシークレットはGCPのSecret Managerで管理されることになります。
https://cloud.google.com/functions/docs/configuring/secrets?hl=ja

defineSecret

具体的な「クレデンシャル等のシークレット情報を含むプログラム」については、defineSecretで生成・定義した変数のことを指します。
https://firebase.google.com/docs/functions/config-env?hl=ja#secret_parameters

defineSecretで扱う注意点と具体的な例題は以下です。

  • 実行する関数のsecretsに事前に設定しておく必要がある
  • 実行中のみvalue()もしはくprocess.envで取得することができる
import {defineSecret} from "firebase-functions/params"
const slackBotToken = defineSecret("SLACK_BOT_TOKEN")
const slackSigningSecret = defineSecret("SLACK_SIGNING_SECRET")

exports.slack = functions.region("asia-northeast1")
.runWith({
  secrets: [slackBotToken, slackSigningSecret],
})
.https
.onRequest((req, resp) => {
  const expressReceiver = new ExpressReceiver({
    signingSecret: slackSigningSecret.value(),
    endpoints: "/events",
    processBeforeResponse: true,
  })
  const app = new App({
    receiver: expressReceiver,
    token: slackBotToken.value(),
    processBeforeResponse: true,
    scopes: ["chat:write", "commands", "users:read"],
  })
  app.command(`/dimbula`, slashCommand.command)
  return expressReceiver.app(req, resp)
})

Functionsのデプロイ時に、Secret Managerに未登録のシークレットは、プロンプトで登録することが出来ます。例えば、HOGEというシークレットが未登録の場合、以下のようになります。ここでシークレットを入力し、EnterすることでデプロイとSecret Managerへの登録が一度に出来ます。

✔  functions: Finished running predeploy script.
i  functions: preparing codebase default for deployment
i  functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i  functions: ensuring required API cloudbuild.googleapis.com is enabled...
i  artifactregistry: ensuring required API artifactregistry.googleapis.com is enabled...
✔  functions: required API cloudbuild.googleapis.com is enabled
✔  artifactregistry: required API artifactregistry.googleapis.com is enabled
✔  functions: required API cloudfunctions.googleapis.com is enabled
i  functions: Loaded environment variables from .env.dimbula-dev.
? This secret will be stored in Cloud Secret Manager (https://cloud.google.com/secret-manager/pricing) as HOGE. Enter a value for HOGE: [input is hidden] 

もし、Secret Managerに登録したシークレットを更新、削除したい場合は、CLIもしくはSecret Manager画面で操作してください。そのあと、適切なバージョンでシークレットを扱えるように、関数のデプロイが必要です。

環境変数

シークレットではない環境変数、例えば、関数のメモリや最小インスタンスを設定したい場合は、defineStringdefineIntが使えます。defineSecretとは異なり、これらは構築中、実行中の両方で利用できます。

const slackMemory = defineInt("SLACK_FUNCTION_MEMORY")

exports.slack = functions.region("asia-northeast1")
.runWith({
  memory: slackMemory,
  // others
})
.https
.onRequest((req, resp) => {
  // do something
})

これら環境変数は、プロジェクトのルート直下に.env.<project_id>のファイルが存在すると、デプロイ時にFirebase Functionsのデプロイ時に設定することが出来ます。前項のFunctionsのログ内にLoaded environment variables from .env.dimbula-devがそれに当たります。

ちなみに、構築時の変数secretsには含める必要はありません。

警告とエラー

シークレットを構築中に利用したり、構築中に環境変数をvalue()で利用しようとすると、正しい使い方ではないことから、デプロイ時のログに以下のようなメッセージが表示されます。

const slackBotToken = defineSecret("SLACK_BOT_TOKEN")
const slackSigningSecret = defineSecret("SLACK_SIGNING_SECRET")
const slackMemory = defineInt("SLACK_FUNCTION_MEMORY")

const expressReceiver = new ExpressReceiver({
  signingSecret: slackSigningSecret.value(),
  endpoints: "/events",
  processBeforeResponse: true,
})
const app = new App({
  receiver: expressReceiver,
  token: slackBotToken.value(),
  processBeforeResponse: true,
  scopes: ["chat:write", "commands", "users:read"],
})
exports.slack = functions.region("asia-northeast1")
.runWith({
  memory: slackMemory.value(),
  // others
})
.https.onRequest(expressReceiver.app)
✔  functions: Finished running predeploy script.
i  functions: preparing codebase default for deployment
i  functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i  functions: ensuring required API cloudbuild.googleapis.com is enabled...
i  artifactregistry: ensuring required API artifactregistry.googleapis.com is enabled...
✔  artifactregistry: required API artifactregistry.googleapis.com is enabled
✔  functions: required API cloudbuild.googleapis.com is enabled
✔  functions: required API cloudfunctions.googleapis.com is enabled
{"severity":"WARNING","message":"params.SLACK_FUNCTION_MEMORY.value() invoked during function deployment, instead of during runtime."}
{"severity":"WARNING","message":"This is usually a mistake. In configs, use Params directly without calling .value()."}
{"severity":"WARNING","message":"example: { memory: memoryParam } not { memory: memoryParam.value() }"}

Error: Failed to load function definition from source: Failed to generate manifest from function source: Error: Cannot access the value of secret "SLACK_SIGNING_SECRET" during function deployment. Secret values are only available at runtime.

この例では、slackMemoryvalue()が不要であること、構築中にslackBotTokenslackSigningSecretを使ったことが原因です。メッセージが指摘するように、構築中にシークレットを使わず、構築中に環境変数を使う場合は、value()を呼び出さない、と修正することで解決できます。

最後に

シークレットと環境変数の取り扱い方が少し違うところがありますので注意が必要ですが、シークレットが実行時のみロードして使える、という背景にはより安全であると理解すると、その違いにも納得できますね。

Discussion