Firebase Functionsでクレデンシャルを扱う
DIMBULA では、SlackアプリやGithubアプリと連携するためのクレデンシャル情報がありますが、Firebase Functionsのプログラムにハードコードせずに、またリポジトリにも含まず、どう安全に扱うことができるか紹介します。
GCP Secret Manager
クレデンシャル等のシークレット情報を含むプログラムをFirebase Functionsにデプロイすると、それらシークレットはGCPのSecret Managerで管理されることになります。
defineSecret
具体的な「クレデンシャル等のシークレット情報を含むプログラム」については、defineSecret
で生成・定義した変数のことを指します。
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画面で操作してください。そのあと、適切なバージョンでシークレットを扱えるように、関数のデプロイが必要です。
環境変数
シークレットではない環境変数、例えば、関数のメモリや最小インスタンスを設定したい場合は、defineString
やdefineInt
が使えます。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.
この例では、slackMemory
のvalue()
が不要であること、構築中にslackBotToken
とslackSigningSecret
を使ったことが原因です。メッセージが指摘するように、構築中にシークレットを使わず、構築中に環境変数を使う場合は、value()
を呼び出さない、と修正することで解決できます。
最後に
シークレットと環境変数の取り扱い方が少し違うところがありますので注意が必要ですが、シークレットが実行時のみロードして使える、という背景にはより安全であると理解すると、その違いにも納得できますね。
Discussion