为什么将环境变量加载到 Firebase 函数时出现未定义?

发布于 2025-01-14 01:44:56 字数 2909 浏览 2 评论 0原文

我正在尝试使用 Google Secrets Manager API 和 Firebase 函数之间的集成将环境变量加载到我的 Firebase 函数中,但是它们全部显示为未定义。我之前使用的是 .env。文件来加载这些变量,在我尝试这个之前它工作得很好,但现在也不起作用!我正在使用 Node.js。

为了在 Secrets API 上设置秘密,我运行:

firebase functions:secrets:set MY_SECRET

我通过在每个秘密上运行以下命令来验证秘密是否已成功设置:

firebase functions:secrets:access MY_SECRET

我在 index.ts 中定义我的函数,如下所示:

import * as functions from 'firebase-functions'
import apiApp from "./api/api"

const REGION = "my region as a string"
const secrets = ["SERVICE_ACCOUNT"]

export const api = functions
  .region(REGION)
  .runWith({ secrets })
  .https.onRequest(apiApp)

在代码中,我使用 process.env.MY_SECRET 访问它们。但是,当我运行 firebaseserve (在 Firebase 模拟器中运行)或 firebasedeploy 时,我总是会收到此错误,后跟由 env 变量导致的堆栈跟踪 <代码>未定义:

Error: Error occurred while parsing your function triggers.

InvalidCharacterError
    at /.../functions/node_modules/base-64/base64.js:23:36
    at Object.<anonymous> (/.../functions/node_modules/base-64/base64.js:164:2)
    at Module._compile (node:internal/modules/cjs/loader:1097:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1151:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:999:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/.../functions/lib/admin.js:5:16)
    at Module._compile (node:internal/modules/cjs/loader:1097:14)

admin.ts:(

import * as admin from 'firebase-admin'
import * as base64 from 'base-64'

const serviceAccount = JSON.parse(base64.decode(process.env.SERVICE_ACCOUNT))
const credential = admin.credential.cert(serviceAccount)

admin.initializeApp({ credential })

...

我对其中一个秘密进行base64解码并收到错误,因为它未定义)

package.json:

{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "build": "tsc",
    "serve": "npm run build && firebase emulators:start --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log",
    "postbuild": "copyfiles -u 1 src/**/*.handlebars src/**/*.json lib/"
  },
  "engines": {
    "node": "16"
  },
  "main": "lib/index.js",
  "dependencies": {
    ...
    "base-64": "^1.0.0",
    "firebase-admin": "^10.0.2",
    "firebase-functions": "^3.18.0",
    ...
  },
  "devDependencies": {
    "@babel/runtime": "^7.17.2",
    "@types/base-64": "^1.0.0",
    ...
  },
  "private": true
}

我尝试修改代码,这样我就不会遇到错误离开,但这仅仅意味着我的端点稍后出错,因为环境变量是未定义

出了什么问题?

I am trying to use the integration between Google Secrets Manager API and Firebase Functions to load environment variables into my Firebase functions, however they are all coming up as undefined. I was previously using .env. files to load these variables, which worked fine before I tried this, but now also isn't working! I'm using Node.js.

To set secrets on the Secrets API, I ran:

firebase functions:secrets:set MY_SECRET

I verified the secrets had been set successfully by running the following on each one:

firebase functions:secrets:access MY_SECRET

I'm defining my functions in index.ts as follows:

import * as functions from 'firebase-functions'
import apiApp from "./api/api"

const REGION = "my region as a string"
const secrets = ["SERVICE_ACCOUNT"]

export const api = functions
  .region(REGION)
  .runWith({ secrets })
  .https.onRequest(apiApp)

And in code, I'm accessing them with process.env.MY_SECRET. However, when I run firebase serve (to run in the Firebase emulator) or firebase deploy, I always get this error followed by a stack trace resulting from the env variable being undefined:

Error: Error occurred while parsing your function triggers.

InvalidCharacterError
    at /.../functions/node_modules/base-64/base64.js:23:36
    at Object.<anonymous> (/.../functions/node_modules/base-64/base64.js:164:2)
    at Module._compile (node:internal/modules/cjs/loader:1097:14)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1151:10)
    at Module.load (node:internal/modules/cjs/loader:975:32)
    at Function.Module._load (node:internal/modules/cjs/loader:822:12)
    at Module.require (node:internal/modules/cjs/loader:999:19)
    at require (node:internal/modules/cjs/helpers:102:18)
    at Object.<anonymous> (/.../functions/lib/admin.js:5:16)
    at Module._compile (node:internal/modules/cjs/loader:1097:14)

admin.ts:

import * as admin from 'firebase-admin'
import * as base64 from 'base-64'

const serviceAccount = JSON.parse(base64.decode(process.env.SERVICE_ACCOUNT))
const credential = admin.credential.cert(serviceAccount)

admin.initializeApp({ credential })

...

(I'm base64 decoding one of the secrets and get an error because it's undefined)

package.json:

{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "scripts": {
    "build": "tsc",
    "serve": "npm run build && firebase emulators:start --only functions",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log",
    "postbuild": "copyfiles -u 1 src/**/*.handlebars src/**/*.json lib/"
  },
  "engines": {
    "node": "16"
  },
  "main": "lib/index.js",
  "dependencies": {
    ...
    "base-64": "^1.0.0",
    "firebase-admin": "^10.0.2",
    "firebase-functions": "^3.18.0",
    ...
  },
  "devDependencies": {
    "@babel/runtime": "^7.17.2",
    "@types/base-64": "^1.0.0",
    ...
  },
  "private": true
}

I've tried modifying the code so I don't run into errors right away, but this just means my endpoints error later on because the env variable is undefined.

What is going wrong?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

尬尬 2025-01-21 01:44:56

由于访问秘密的方式不正确,您正在做的事情将导致未定义。在此代码段中:

import * as functions from 'firebase-functions'
import apiApp from "./api/api"

const REGION = "my region as a string"
const secrets = ["SERVICE_ACCOUNT"]

export const api = functions
  .region(REGION)
  .runWith({ secrets })
  .https.onRequest(apiApp)

您将 secret 添加到 env 变量中,然后该变量只能在 .https.onRequest(apiApp)。例如,

app.get('/', (req, res) => {
    console.log(process.env.SERVICE_ACCOUNT);
    return res.send(`Done!`);
  });

const secrets = ["SERVICE_ACCOUNT"];

export const api = functions
.region('us-central1')
.runWith({ secrets })
.https.onRequest(app);

上面的代码将在您传递给的函数上记录 SERVICE_ACCOUNT 秘密。此文档中也有说明:

只有在其 runWith 参数中专门包含机密的函数才能以环境变量的形式访问该机密。这有助于您确保秘密值仅在需要时可用,从而降低意外泄露秘密的风险。


为了能够在不使用 https 函数的 .runWith 参数的情况下访问您的密钥,您必须首先安装 @google-cloud/secret-manager

npm i @google-cloud/secret-manager

然后启动它:

import {SecretManagerServiceClient} from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();

访问您的秘密版本:

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const name = 'projects/my-project/secrets/my-secret/versions/5';
// const name = 'projects/my-project/secrets/my-secret/versions/latest';

async function accessSecretVersion() {
  const [version] = await client.accessSecretVersion({
    name: name,
  });

  // Extract the payload as a string.
  const payload = version.payload.data.toString();

  // WARNING: Do not print the secret in a production environment - this
  // snippet is showing how to access the secret material.
  console.info(`Payload: ${payload}`);
}

accessSecretVersion();

作为参考,这里是根据您的情况编译的代码admin.ts

import * as admin from 'firebase-admin';
import {SecretManagerServiceClient} from '@google-cloud/secret-manager';
import * as base64 from 'base-64';

const client = new SecretManagerServiceClient();

// Must follow expected format: projects/*/secrets/*/versions/*
// You can always use `latest` if you want to use the latest uploaded version.
const name = 'projects/<PROJECT-ID>/secrets/SERVICE_ACCOUNT/versions/latest'
  
 let credentials: admin.app.App;

 export const db = async (): Promise<admin.app.App> => {
  if (credentials) {
      return credentials;
  } else {
      const [version] = await client.accessSecretVersion({
          name: name
      });
      const result: any = JSON.parse(version?.payload?.data?.toString());
      const params = {
          type: result.type,
          projectId: result.project_id,
          privateKeyId: result.private_key_id,
          privateKey: result.private_key,
          clientEmail: result.client_email,
          clientId: result.client_id,
          authUri: result.auth_uri,
          tokenUri: result.token_uri,
          authProviderX509CertUrl: result.auth_provider_x509_cert_url,
          clientC509CertUrl: result.client_x509_cert_url,
      };
      credentials = admin.initializeApp({
          credential: admin.credential.cert(params),
          storageBucket: `gs://${result.project_id}.appspot.com`,
      });
      return credentials;
  }
};

然后您可以导入admin.ts并使用这些方法调用db

有关更多信息,请查看这些文档:

您可能还想查看Secret Manager 最佳实践

What you're doing will result in undefined due to incorrect ways of accessing secrets. In this code snippet:

import * as functions from 'firebase-functions'
import apiApp from "./api/api"

const REGION = "my region as a string"
const secrets = ["SERVICE_ACCOUNT"]

export const api = functions
  .region(REGION)
  .runWith({ secrets })
  .https.onRequest(apiApp)

You're adding the secret to the env variables which then can only be used on the .https.onRequest(apiApp). E.g.

app.get('/', (req, res) => {
    console.log(process.env.SERVICE_ACCOUNT);
    return res.send(`Done!`);
  });

const secrets = ["SERVICE_ACCOUNT"];

export const api = functions
.region('us-central1')
.runWith({ secrets })
.https.onRequest(app);

The above code will log the SERVICE_ACCOUNT secret on the function which you passed to. It's also stated in this documentation:

Only functions that specifically include a secret in their runWith parameter will have access to that secret as an environment variable. This helps you make sure that secret values are only available where they're needed, reducing the risk of accidentally leaking a secret.


For you to be able to access your secret without using the .runWith parameter of the https functions, you must first install the @google-cloud/secret-manager:

npm i @google-cloud/secret-manager

then initiate it:

import {SecretManagerServiceClient} from '@google-cloud/secret-manager';
const client = new SecretManagerServiceClient();

Accessing your secret versions:

/**
 * TODO(developer): Uncomment these variables before running the sample.
 */
// const name = 'projects/my-project/secrets/my-secret/versions/5';
// const name = 'projects/my-project/secrets/my-secret/versions/latest';

async function accessSecretVersion() {
  const [version] = await client.accessSecretVersion({
    name: name,
  });

  // Extract the payload as a string.
  const payload = version.payload.data.toString();

  // WARNING: Do not print the secret in a production environment - this
  // snippet is showing how to access the secret material.
  console.info(`Payload: ${payload}`);
}

accessSecretVersion();

For reference, here's the compiled code based on your admin.ts:

import * as admin from 'firebase-admin';
import {SecretManagerServiceClient} from '@google-cloud/secret-manager';
import * as base64 from 'base-64';

const client = new SecretManagerServiceClient();

// Must follow expected format: projects/*/secrets/*/versions/*
// You can always use `latest` if you want to use the latest uploaded version.
const name = 'projects/<PROJECT-ID>/secrets/SERVICE_ACCOUNT/versions/latest'
  
 let credentials: admin.app.App;

 export const db = async (): Promise<admin.app.App> => {
  if (credentials) {
      return credentials;
  } else {
      const [version] = await client.accessSecretVersion({
          name: name
      });
      const result: any = JSON.parse(version?.payload?.data?.toString());
      const params = {
          type: result.type,
          projectId: result.project_id,
          privateKeyId: result.private_key_id,
          privateKey: result.private_key,
          clientEmail: result.client_email,
          clientId: result.client_id,
          authUri: result.auth_uri,
          tokenUri: result.token_uri,
          authProviderX509CertUrl: result.auth_provider_x509_cert_url,
          clientC509CertUrl: result.client_x509_cert_url,
      };
      credentials = admin.initializeApp({
          credential: admin.credential.cert(params),
          storageBucket: `gs://${result.project_id}.appspot.com`,
      });
      return credentials;
  }
};

You can then import admin.ts and call db with these method.

For more information, check out these documentations:

You may also want to checkout Secret Manager Best Practices.

鲸落 2025-01-21 01:44:56

我遇到了这个问题,因为我的导入导致在 index.ts 中调用 admin.initialiseApp。

index.ts:

import apiApp from "./api/api"

...

api.ts 通过许多其他文件导入 admin.ts,而 admin.ts 需要填充 process.env.SERVICE_ACCOUNT。正如 Marc Anthony B 所说,如果从 index.ts 调用,SERVICE_ACCOUNT 还不会被填充,因此会出现错误。

我通过将 admin.ts 重构为以下内容来解决:

import * as admin from "firebase-admin";
import * as base64 from "base-64";

let dbInstance: admin.firestore.Firestore | null = null;
let authInstance: admin.auth.Auth | null = null;

function getAdmin() {
  const serviceAccount = JSON.parse(base64.decode(process.env.SERVICE_ACCOUNT));
  const credential = admin.credential.cert(serviceAccount);

  admin.initializeApp({ credential });

  dbInstance = admin.firestore();
  authInstance = admin.auth();

  return { db: dbInstance, auth: authInstance };
}

export const db = () => dbInstance || getAdmin().db;
export const auth = () => authInstance || getAdmin().auth;

因此我所有的导出都是函数,而不是 db 和 auth 的实例。

I ran into this issue because my imports were leading to admin.initialiseApp being called in index.ts.

index.ts:

import apiApp from "./api/api"

...

api.ts was importing admin.ts through a number of other files, and admin.ts required process.env.SERVICE_ACCOUNT to be populated. As Marc Anthony B said, SERVICE_ACCOUNT wouldn't yet be populated if calling from index.ts, hence the error.

I solved by refactoring admin.ts to the following:

import * as admin from "firebase-admin";
import * as base64 from "base-64";

let dbInstance: admin.firestore.Firestore | null = null;
let authInstance: admin.auth.Auth | null = null;

function getAdmin() {
  const serviceAccount = JSON.parse(base64.decode(process.env.SERVICE_ACCOUNT));
  const credential = admin.credential.cert(serviceAccount);

  admin.initializeApp({ credential });

  dbInstance = admin.firestore();
  authInstance = admin.auth();

  return { db: dbInstance, auth: authInstance };
}

export const db = () => dbInstance || getAdmin().db;
export const auth = () => authInstance || getAdmin().auth;

So all my exports were functions, instead of instances of db and auth.

温柔戏命师 2025-01-21 01:44:56

我遇到了类似的问题,当尝试使用 initializeApp() 时,环境变量未定义

@mef27 的解决方案也对我有用。但我正在使用较新版本的 Firebase Admin sdk。

所以对于 2023 年读到这篇文章的人来说,这对我有用:

import { getAuth, Auth } from "firebase-admin/auth";
import { getStorage, Storage } from "firebase-admin/storage";
import { getApp, getApps, initializeApp, cert } from "firebase-admin/app";

let authInstance: Auth | null = null;
let storageInstance: ReturnType<Storage["bucket"]> | null = null;

function getAdmin() {
  const app = !getApps().length
    ? initializeApp({
        credential: cert({
          projectId: process.env.FB_PROJECT_ID,
          clientEmail: process.env.FB_CLIENT_EMAIL,
          privateKey: process.env.FB_PRIVATE_KEY,
        }),
        storageBucket: process.env.FB_STORAGE_BUCKET,
      })
    : getApp();

  authInstance = getAuth(app);
  storageInstance = getStorage(app).bucket();

  return { auth: authInstance, storage: storageInstance };
}

export const auth = () => authInstance ?? getAdmin().auth;
export const storage = () => storageInstance ?? getAdmin().storage;

I had a similar issue where the environment variables where undefined when trying to use initializeApp()

The solution of @mef27 also worked for me. But i'm using a newer version of the Firebase Admin sdk.

So for anyone reading this in 2023, this worked for me:

import { getAuth, Auth } from "firebase-admin/auth";
import { getStorage, Storage } from "firebase-admin/storage";
import { getApp, getApps, initializeApp, cert } from "firebase-admin/app";

let authInstance: Auth | null = null;
let storageInstance: ReturnType<Storage["bucket"]> | null = null;

function getAdmin() {
  const app = !getApps().length
    ? initializeApp({
        credential: cert({
          projectId: process.env.FB_PROJECT_ID,
          clientEmail: process.env.FB_CLIENT_EMAIL,
          privateKey: process.env.FB_PRIVATE_KEY,
        }),
        storageBucket: process.env.FB_STORAGE_BUCKET,
      })
    : getApp();

  authInstance = getAuth(app);
  storageInstance = getStorage(app).bucket();

  return { auth: authInstance, storage: storageInstance };
}

export const auth = () => authInstance ?? getAdmin().auth;
export const storage = () => storageInstance ?? getAdmin().storage;

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文