【问题标题】:App Engine can't find default credentials to connect to Firestore in Google CloudApp Engine 在 Google Cloud 中找不到连接到 Firestore 的默认凭据
【发布时间】:2022-02-14 21:13:17
【问题描述】:

我有一个在 Google App Engine 上运行的 NextJS Typescript 应用。它从 Firestore 获取数据,一切正常。为了提高应用程序的速度,我正在试验新的数据获取基础架构,其中服务器侦听 Firestore 集合,并在 Firestore 中进行更改时将所有数据更新到 tmp 文件夹中的 JSON 文件。这样,所有数据都是最新的,并且始终可供 App Engine 使用。在本地,这就像一个魅力。

有一些明显的事情我需要改进,但对我来说下一步是在 GCP 中运行一个开发项目,看看我的内存使用情况是否正常,以及它是否能像我希望的那样快速运行等等。但问题是当我更改 NextJS 基础结构以包含自定义服务器时,App Engine 和 Firestore 之间的连接消失了。

我在 GCP 日志中看到的问题是:

Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
    at GoogleAuth.getApplicationDefaultAsync (/workspace/node_modules/google-auth-library/build/src/auth/googleauth.js:180:19)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at runNextTicks (node:internal/process/task_queues:65:3)
    at listOnTimeout (node:internal/timers:526:9)
    at processTimers (node:internal/timers:500:7)
    at async GoogleAuth.getClient (/workspace/node_modules/google-auth-library/build/src/auth/googleauth.js:558:17)
    at async GrpcClient._getCredentials (/workspace/node_modules/google-gax/build/src/grpc.js:145:24)
    at async GrpcClient.createStub (/workspace/node_modules/google-gax/build/src/grpc.js:308:23)

客户端中的实际错误消息是“502 Bad Gateway – nginx”。

之前我有一个基本的 NextJS 应用程序,它有前端页面和后端 API 路由。路由连接到 Firestore 并将该数据提供给正确的用户等。主要区别在于我添加了一个自定义服务器来启动侦听器:

import { Firestore } from '@google-cloud/firestore';
import express, { Request, Response } from 'express';
import next from 'next';
import fs from 'fs';
import os from 'os';

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const port = process.env.PORT || 3000;

let firestoreListeners: { [collectionId: string]: () => void } = {};

const unsubscribeAllListeners = () => {
    for (const collectionId of Object.keys(firestoreListeners)) {
        console.log('unsubscribing from', collectionId);
        firestoreListeners[collectionId]();
    }
    firestoreListeners = {};
};

const skippedCollections = ['analytics', 'pageRevisions', 'newsItemRevisions'];

app.prepare().then(() => {
    const server = express();

    unsubscribeAllListeners();

    const firestoreSettings = {} as FirebaseFirestore.Settings;
    if (process.env.GCP_KEYFILE_NAME) {
        firestoreSettings.keyFilename = process.env.GCP_KEYFILE_NAME;
    }

    const firestoreData: {
        [collectionId: string]: {
            [id: string]: any;
        };
    } = {};
    const firestore = new Firestore(firestoreSettings);

    firestore.listCollections().then((collections) => {
        for (const collection of collections) {
            if (
                !firestoreListeners[collection.id] &&
                !skippedCollections.includes(collection.id)
            ) {
                console.log('listening to', collection.id);
                firestoreData[collection.id] = {};
                const listener = firestore
                    .collection(collection.id)
                    .onSnapshot((snapshot) => {
                        firestoreData[collection.id] = {};
                        for (const doc of snapshot.docs) {
                            firestoreData[collection.id][doc.id] = {
                                _id: doc.id,
                                ...doc.data(),
                            };
                        }
                        if (!fs.existsSync(os.tmpdir() + '/data')) {
                            fs.mkdirSync(os.tmpdir() + '/data');
                        }
                        fs.writeFileSync(
                            os.tmpdir() + `/data/${collection.id}.json`,
                            JSON.stringify(firestoreData[collection.id])
                        );

                        console.log(
                            'updated',
                            collection.id,
                            'with',
                            snapshot.docs.length,
                            'docs'
                        );
                    });
                firestoreListeners[collection.id] = listener;
            }
        }
    });

    server.all('*', (req: Request, res: Response) => {
        return handle(req, res);
    });
    server.listen(port, (err?: any) => {
        if (err) throw err;
        console.log(
            `> Ready on localhost:${port} - env ${process.env.NODE_ENV}`
        );
    });
    server.on('close', function () {
        unsubscribeAllListeners();
    });
    process.on('beforeExit', () => {
        unsubscribeAllListeners();
    });
});

构建和部署脚本没问题,如果我将侦听器逻辑排除在外,只部署自定义服务器,它就可以工作。

有什么问题?是 nginx 的问题还是我有其他问题?

【问题讨论】:

  • 如果我将监听器逻辑移动到 /_ah/warmup 路由,它会工作,然后每个路由都会启动,但是很难确保每个实例只有一个监听器。但关键可能是它与 NextJS API 路由中的侦听器逻辑一起使用,但当我使用服务器完成侦听器逻辑时则不行。
  • 请发表您的评论作为答案,以便社区发现这很有帮助。
  • 我不觉得我的评论真的是一个答案。

标签: node.js google-app-engine google-cloud-platform google-cloud-firestore next.js


【解决方案1】:

问题显然是我无法在listen 之前甚至在listen 回调之前启动我的Firestore 连接。我必须稍后再做(让 GAE 有可能对 Firestore 进行身份验证?)。

当我移动我的听众来收听所有端点时,它起作用了。下面是一个有助于解决问题的解决方案。我觉得它不是那么漂亮,但完成了工作。

import { Firestore } from '@google-cloud/firestore';
import express, { Request, Response } from 'express';
import next from 'next';
import fs from 'fs';
import os from 'os';

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();
const port = process.env.PORT || 3000;

let firestoreListeners: { [collectionId: string]: () => void } = {};

const unsubscribeAllListeners = () => {
    for (const collectionId of Object.keys(firestoreListeners)) {
        console.log('unsubscribing from', collectionId);
        firestoreListeners[collectionId]();
    }
    firestoreListeners = {};
};

const skippedCollections = ['analytics', 'pageRevisions', 'newsItemRevisions'];

export const firestoreData: {
    [collectionId: string]: {
        [id: string]: any;
    };
} = {};

let listenersInitiated = false;

const initiateListeners = () => {
    if (listenersInitiated) {
        return;
    }
    const firestoreSettings = {} as FirebaseFirestore.Settings;
    if (process.env.GCP_KEYFILE_NAME) {
        firestoreSettings.keyFilename = process.env.GCP_KEYFILE_NAME;
    }
    const firestore = new Firestore(firestoreSettings);

    firestore.listCollections().then((collections) => {
        for (const collection of collections) {
            if (
                !firestoreListeners[collection.id] &&
                !skippedCollections.includes(collection.id)
            ) {
                console.log('listening to', collection.id);
                firestoreData[collection.id] = {};
                const listener = firestore
                    .collection(collection.id)
                    .onSnapshot((snapshot) => {
                        firestoreData[collection.id] = {};
                        for (const doc of snapshot.docs) {
                            firestoreData[collection.id][doc.id] = {
                                _id: doc.id,
                                ...doc.data(),
                            };
                        }
                        if (!fs.existsSync(os.tmpdir() + '/data')) {
                            fs.mkdirSync(os.tmpdir() + '/data');
                        }
                        fs.writeFileSync(
                            os.tmpdir() + `/data/${collection.id}.json`,
                            JSON.stringify(firestoreData[collection.id])
                        );

                        console.log(
                            'updated',
                            collection.id,
                            'with',
                            snapshot.docs.length,
                            'docs'
                        );
                    });
                firestoreListeners[collection.id] = listener;
            }
        }
    });
    listenersInitiated = true;
};

app.prepare().then(() => {
    const server = express();
    unsubscribeAllListeners();

    server.all('*', (req: Request, res: Response) => {
        initiateListeners();
        return handle(req, res);
    });
    server.listen(port, (err?: any) => {
        if (err) throw err;
        console.log(
            `> Ready on localhost:${port} - env ${process.env.NODE_ENV}`
        );
    });
    server.on('close', function () {
        console.log('Closing');
        unsubscribeAllListeners();
    });
    process.on('beforeExit', () => {
        console.log('Closing');
        unsubscribeAllListeners();
    });
});

根据我最初的测试,这在 GAE 中运行良好。正确设置 app.yaml 设置后,速度快且成本低。

如果服务器实例存在很长时间,这并不能真正处理侦听器失败,而且它可能会启动太多侦听器,但我的测试的初步结果是有希望的!

【讨论】:

    猜你喜欢
    • 2017-05-04
    • 1970-01-01
    • 2020-06-07
    • 2015-09-15
    • 2015-10-17
    • 1970-01-01
    • 2020-07-03
    • 2017-06-17
    • 2018-08-15
    相关资源
    最近更新 更多