遇到 540 秒超时限制后,如何从 Firebase Functions 迁移到 Cloud 运行?
How to move from Firebase Functions to Cloud Run after encountering 540s timeout limit?
我正在阅读 this Reddit thread,其中一位用户提到 540 秒是 Firebase Functions 的限制,建议迁移到云端 运行。
As others have said 540s is the maximum timeout and if you want to increase it without changing much else about your code, consider moving to Cloud Run. - @samtstern on Reddit
看完 Node.JS QuickStart documentation
以及 YouTube 和 Google 上的其他内容,我没有找到解释如何将 Firebase 函数移动到云端的好指南 运行。
我阅读的内容没有解决的问题之一,例如:我用什么替换 firebase-functions
包来定义函数?等等...
那么,我怎样才能将我的 Firebase 函数移到云端 运行 而不是 运行 进入 540 秒的最大超时限制?
const functions = require('firebase-functions');
const runtimeOpts = {timeoutSeconds: 540,memory: '2GB'}
exports.hourlyData = functions.runWith(runtimeOpts).pubsub.schedule('every 1 hours')
前言: 以下步骤已针对更广泛的受众进行推广,而不仅仅是 OP 的问题(涵盖 HTTP 事件、计划和 Pub/Sub 函数)并进行了调整来自问题中链接的文档:Deploying Node.JS Images on Cloud Run.
第 0 步:Code/Architecture审核
通常情况下,超过 9 分钟的 Cloud Function 超时是您代码中的错误造成的 - 请确保在切换到 Cloud 运行 之前对此进行评估,因为这只会使问题更糟。其中最常见的是顺序而不是并行异步处理(通常是由于在 for
/while
循环中使用 await
)。
如果您的代码正在执行需要很长时间的有意义的工作,请考虑将其拆分为可以并行处理输入数据的“子函数”。您可以使用单个函数触发一个函数的多个实例,而不是为数据库中的每个用户处理数据,每个实例处理不同的用户 ID 范围,例如 a-l\uf8ff
、m-z\uf8ff
、A-L\uf8ff
、M-Z\uf8ff
和 0-9\uf8ff
。
最后,Cloud 运行 和 Cloud Functions 非常相似,它们旨在接收请求、处理请求然后 return 响应。 Cloud Functions 的限制为 9 分钟,Cloud 运行s 的限制为 60 分钟。一旦该响应完成(因为服务器结束响应、客户端丢失连接或客户端中止请求),实例将被严重限制或终止。虽然您可以在使用 Cloud 运行 时使用 WebSockets 和 gRPC 在服务器和客户端之间进行持久通信,但它们仍然受到此限制。有关详细信息,请参阅 Cloud Run: General development tips 文档。
与其他无服务器解决方案一样,您的客户端和服务器需要能够处理与不同实例的连接。您的代码不应使用本地状态(例如 session 数据的本地存储)。有关详细信息,请参阅 Setting request timeout 文档。
第 1 步:安装 Google Cloud SDK
这一步我会推荐你参考Installing Google Cloud SDK documentation。
安装后,调用 gcloud auth login
并使用用于目标 Firebase 项目的帐户登录。
第 2 步:获取您的 Firebase 项目设置
打开你的project settings in the Firebase Console并记下你的项目ID和你的默认GCP资源位置。
Firebase Functions 和 Cloud 运行 实例应尽可能 co-located 与您的 GCP 资源。在 Firebase Functions 中,这是通过 changing the region in code 并使用 CLI 部署来实现的。对于 Cloud 运行,您可以在命令行中将这些参数指定为标志(或使用 Google Cloud Console)。为了下面的说明和简单起见,我将使用 us-central1
作为我的 默认 GCP 资源位置 是 nam5 (us-central)
.
如果在您的项目中使用 Firebase 实时数据库,请访问您的 RTDB settings in the Firebase Console 并记下您的 数据库 URL。这通常是 https://PROJECT_ID.firebaseio.com/
.
的形式
如果在您的项目中使用 Firebase 存储,请访问您的 Cloud Storage settings in the Firebase Console 并记下您的 Bucket URI。从这个 URI 中,我们需要注意主机(忽略 gs://
部分),它通常采用 PROJECT_ID.appspot.com
.
的形式
这里有一个 table,您可以复制它以帮助跟踪:
Project ID:
PROJECT_ID
Database URL:
https://PROJECT_ID.firebaseio.com
Storage Bucket:
PROJECT_ID.appspot.com
Default GCP Resource Location:
Chosen Cloud Run Region:
步骤 3:创建目录
在您的 Firebase 项目目录或您选择的目录中,创建一个新的 cloudrun
文件夹。
不同于 Firebase Cloud Functions,您可以在单个代码模块中定义多个函数,每个 Cloud 运行 图像使用自己的代码模块。因此,每个 Cloud 运行 图像都应存储在自己的目录中。
因为我们要定义一个名为 helloworld
的 Cloud 运行 实例,所以我们将在 cloudrun
.
中创建一个名为 helloworld
的目录
mkdir cloudrun
mkdir cloudrun/helloworld
cd cloudrun/helloworld
第 4 步:创建 package.json
为了正确部署 Cloud 运行 映像,我们需要提供一个 package.json
用于在部署的容器中安装依赖项。
package.json
文件的格式类似于:
{
"name": "SERVICE_NAME",
"description": "",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "node index.js"
"image": "gcloud builds submit --tag gcr.io/PROJECT_ID/SERVICE_NAME --project PROJECT_ID",
"deploy:public": "gcloud run deploy SERVICE_NAME --image gcr.io/PROJECT_ID/SERVICE_NAME --allow-unauthenticated --region REGION_ID --project PROJECT_ID",
"deploy:private": "gcloud run deploy SERVICE_NAME --image gcr.io/PROJECT_ID/SERVICE_NAME --no-allow-unauthenticated --region REGION_ID --project PROJECT_ID",
"describe": "gcloud run services describe SERVICE_NAME --region REGION_ID --project PROJECT_ID --platform managed",
"find": "gcloud run services describe SERVICE_NAME --region REGION_ID --project PROJECT_ID --platform managed --format='value(status.url)'"
},
"engines": {
"node": ">= 12.0.0"
},
"author": "You",
"license": "Apache-2.0",
"dependencies": {
"express": "^4.17.1",
"body-parser": "^1.19.0",
/* ... */
},
"devDependencies": {
/* ... */
}
}
在上面的文件中,SERVICE_NAME
、REGION_ID
和 PROJECT_ID
将根据步骤 2 中的详细信息进行适当换出。我们还安装了 express
和body-parser
处理传入的请求。
还有一些模块脚本可以帮助部署。
Script Name
Description
image
Submits the image to Cloud Build to be added to the Container Registry for other commands.
deploy:public
Deploys the image from the above command to be used by Cloud Run (while allowing any requester to invoke it) and returns its service URL (which is partly randomized).
deploy:private
Deploys the image from the above command to be used by Cloud Run (while requiring that the requester that invokes it is an authorized user/service account) and returns its service URL (which is partly randomized).
describe
Gets the statistics & configuration of the deployed Cloud Run.
find
Extracts only the service URL from the response of npm run describe
注意:这里的“授权用户”是指与项目关联的Google帐户,而不是普通的Firebase用户。要允许 Firebase 用户调用您的云 运行,您必须使用 deploy:public
部署它并在您的云 运行 代码中处理令牌验证,适当地拒绝请求。
作为此文件填写的示例,您会得到:
{
"name": "helloworld",
"description": "Simple hello world sample in Node with Firebase",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "node index.js"
"image": "gcloud builds submit --tag gcr.io/com-example-cloudrun/helloworld --project com-example-cloudrun",
"deploy:public": "gcloud run deploy helloworld --image gcr.io/com-example-cloudrun/helloworld --allow-unauthenticated --region us-central1 --project com-example-cloudrun",
"deploy:public": "gcloud run deploy helloworld --image gcr.io/com-example-cloudrun/helloworld --no-allow-unauthenticated --region us-central1 --project com-example-cloudrun",
"describe": "gcloud run services describe helloworld --region us-central1 --project com-example-cloudrun --platform managed",
"find": "gcloud run services describe helloworld --region us-central1 --project com-example-cloudrun --platform managed --format='value(status.url)'"
},
"engines": {
"node": ">= 12.0.0"
},
"author": "You",
"license": "Apache-2.0",
"dependencies": {
/* ... */
},
"devDependencies": {
/* ... */
}
}
第 5 步:创建容器文件
告诉 Cloud Build 为您的云使用什么容器运行 图片,您必须为您的图片创建一个 Dockerfile
。为防止向服务器发送错误的文件,您还应该指定一个 .dockerignore
文件。
在此文件中,我们使用第 2 步中的 Firebase 项目设置重新创建 process.env.FIREBASE_CONFIG
环境变量。此变量由 Firebase Admin SDK 使用并包含以下信息作为 JSON 字符串:
{
databaseURL: "https://PROJECT_ID.firebaseio.com",
storageBucket: "PROJECT_ID.appspot.com",
projectId: "PROJECT_ID"
}
这里是cloudrun/helloworld/Dockerfile
:
# Use the official lightweight Node.js 14 image.
# https://hub.docker.com/_/node
FROM node:14-slim
# Create and change to the app directory.
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./
# Install production dependencies.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN npm install --only=production
# Copy local code to the container image.
COPY . ./
# Define default configuration for Admin SDK
# databaseURL is usually "https://PROJECT_ID.firebaseio.com", but may be different.
# TODO: Update me
ENV FIREBASE_CONFIG={"databaseURL":"https://PROJECT_ID.firebaseio.com","storageBucket":"PROJECT_ID.appspot.com","projectId":"PROJECT_ID"}
# Run the web service on container startup.
CMD [ "node", "index.js" ]
这里是cloudrun/helloworld/.dockerignore
:
Dockerfile
.dockerignore
node_modules
npm-debug.log
第 6 步:创建和部署您的入口点
启动新的 Cloud 运行 实例时,它通常会使用 PORT
environment variable.
指定它希望您的代码侦听的端口
变体:迁移 HTTP 事件函数
当您使用 HTTP Event function from the firebase-functions
package, it internally handles body-parsing on your behalf. The Functions Framework uses the body-parser
package for this and defines the parsers here.
要处理用户授权,您可以使用这个 validateFirebaseIdToken()
中间件来检查随请求提供的 ID 令牌。
对于 HTTP-based 云 运行,需要配置 CORS 才能从浏览器调用它。这可以通过安装 cors
package 并适当配置来完成。在下面的示例中,cors
将反映发送给它的来源。
const express = require('express');
const cors = require('cors')({origin: true});
const app = express();
app.use(cors);
// To replicate a Cloud Function's body parsing, refer to
// https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/d894b490dda7c5fd4690cac884fd9e41a08b6668/src/server.ts#L47-L95
app.use(/* body parsers */);
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
// Start of your handlers
app.get('/', (req, res) => {
const name = process.env.NAME || 'World';
res.send(`Hello ${name}!`);
});
// End of your handlers
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});
在$FIREBASE_PROJECT_DIR/cloudrun/helloworld
目录中,执行以下命令来部署你的镜像:
npm run image // builds container & stores to container repository
npm run deploy:public // deploys container image to Cloud Run
变体:使用 Cloud Scheduler 调用
使用 Cloud Scheduler 调用 Cloud 运行 时,您可以选择调用它的方法(GET
、POST
(默认)、PUT
, HEAD
, DELETE
).要复制 Cloud Functions 的 data
和 context
参数,最好使用 POST
,因为这些参数随后将在请求的 body 中传递。与 Firebase Functions 一样,这些来自 Cloud Scheduler 的请求可能会被重试,因此请确保 handle idempotency appropriately.
注意: 即使 Cloud Scheduler 调用请求的 body 是 JSON-formatted,该请求也会使用 Content-Type: text/plain
提供,这我们需要处理。
此代码改编自 Functions Framework source(Google LLC、Apache 2.0)
const express = require('express');
const { json } = require('body-parser');
async function handler(data, context) {
/* your logic here */
const name = process.env.NAME || 'World';
console.log(`Hello ${name}!`);
}
const app = express();
// Cloud Scheduler requests contain JSON using
"Content-Type: text/plain"
app.use(json({ type: '*/*' }));
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
app.post('/*', (req, res) => {
const event = req.body;
let data = event.data;
let context = event.context;
if (context === undefined) {
// Support legacy events and CloudEvents in structured content mode, with
// context properties represented as event top-level properties.
// Context is everything but data.
context = event;
// Clear the property before removing field so the data object
// is not deleted.
context.data = undefined;
delete context.data;
}
Promise.resolve()
.then(() => handler(data, context))
.then(
() => {
// finished without error
// the return value of `handler` is ignored because
// this isn't a callable function
res.sendStatus(204); // No content
},
(err) => {
// handler threw error
console.error(err.stack);
res.set('X-Google-Status', 'error');
// Send back the error's message (as calls to this endpoint
// are authenticated project users/service accounts)
res.send(err.message);
}
)
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});
注意: Functions Framework 通过发回带有 X-Google-Status: error
header 的 HTTP 200 OK
响应来处理错误。这实际上意味着“成功失败”。作为局外人,我不确定为什么要这样做,但我可以假设这样做是为了让调用者知道不必费心重试该函数 - 它只会得到相同的结果。
在$FIREBASE_PROJECT_DIR/cloudrun/helloworld
目录中,执行以下命令来部署你的镜像:
npm run image // builds container & stores to container repository
npm run deploy:private // deploys container image to Cloud Run
注意:在下面的设置命令中(只需要运行这一次),PROJECT_ID
,SERVICE_NAME
,SERVICE_URL
和 IAM_ACCOUNT
需要酌情替换。
接下来我们需要create a service account that Cloud Scheduler can use to invoke the Cloud Run. You can call it whatever you want such as scheduled-run-invoker
. The email of this service account will be referred to as IAM_ACCOUNT
in the next step. This Google Cloud Tech YouTube video (starts at the right spot, about 15s) will quickly show what you need to do. Once you've created the account, you can create the Cloud Scheduler job跟随视频的下一个 30 秒左右或使用以下命令:
gcloud scheduler jobs create http scheduled-run-SERVICE_NAME /
--schedule="every 1 hours" /
--uri SERVICE_URL /
--attempt-deadline 60m /
--http-method post /
--message-body='{"optional-custom-data":"here","if-you":"want"}' /
--oidc-service-account-email IAM_ACCOUNT
--project PROJECT_ID
您的云 运行 现在应该已安排。
变体:使用 Pub/Sub
调用
据我了解,部署过程与计划的 运行 (deploy:private
) 相同,但我不确定具体细节。但是,这里是 Pub/Sub 解析器的 Cloud 运行 源代码:
此代码改编自 Functions Framework source(Google LLC、Apache 2.0)
const express = require('express');
const { json } = require('body-parser');
const PUBSUB_EVENT_TYPE = 'google.pubsub.topic.publish';
const PUBSUB_MESSAGE_TYPE =
'type.googleapis.com/google.pubsub.v1.PubsubMessage';
const PUBSUB_SERVICE = 'pubsub.googleapis.com';
/**
* Extract the Pub/Sub topic name from the HTTP request path.
* @param path the URL path of the http request
* @returns the Pub/Sub topic name if the path matches the expected format,
* null otherwise
*/
const extractPubSubTopic = (path: string): string | null => {
const parsedTopic = path.match(/projects\/[^/?]+\/topics\/[^/?]+/);
if (parsedTopic) {
return parsedTopic[0];
}
console.warn('Failed to extract the topic name from the URL path.');
console.warn(
"Configure your subscription's push endpoint to use the following path: ",
'projects/PROJECT_NAME/topics/TOPIC_NAME'
);
return null;
};
async function handler(message, context) {
/* your logic here */
const name = message.json.name || message.json || 'World';
console.log(`Hello ${name}!`);
}
const app = express();
// Cloud Scheduler requests contain JSON using
"Content-Type: text/plain"
app.use(json({ type: '*/*' }));
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
app.post('/*', (req, res) => {
const body = req.body;
if (!body) {
res.status(400).send('no Pub/Sub message received');
return;
}
if (typeof body !== "object" || body.message === undefined) {
res.status(400).send('invalid Pub/Sub message format');
return;
}
const context = {
eventId: body.message.messageId,
timestamp: body.message.publishTime || new Date().toISOString(),
eventType: PUBSUB_EVENT_TYPE,
resource: {
service: PUBSUB_SERVICE,
type: PUBSUB_MESSAGE_TYPE,
name: extractPubSubTopic(req.path),
},
};
// for storing parsed form of body.message.data
let _jsonData = undefined;
const data = {
'@type': PUBSUB_MESSAGE_TYPE,
data: body.message.data,
attributes: body.message.attributes || {},
get json() {
if (_jsonData === undefined) {
const decodedString = Buffer.from(base64encoded, 'base64')
.toString('utf8');
try {
_jsonData = JSON.parse(decodedString);
} catch (parseError) {
// fallback to raw string
_jsonData = decodedString;
}
}
return _jsonData;
}
};
Promise.resolve()
.then(() => handler(data, context))
.then(
() => {
// finished without error
// the return value of `handler` is ignored because
// this isn't a callable function
res.sendStatus(204); // No content
},
(err) => {
// handler threw error
console.error(err.stack);
res.set('X-Google-Status', 'error');
// Send back the error's message (as calls to this endpoint
// are authenticated project users/service accounts)
res.send(err.message);
}
)
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});
我正在阅读 this Reddit thread,其中一位用户提到 540 秒是 Firebase Functions 的限制,建议迁移到云端 运行。
As others have said 540s is the maximum timeout and if you want to increase it without changing much else about your code, consider moving to Cloud Run. - @samtstern on Reddit
看完 Node.JS QuickStart documentation 以及 YouTube 和 Google 上的其他内容,我没有找到解释如何将 Firebase 函数移动到云端的好指南 运行。
我阅读的内容没有解决的问题之一,例如:我用什么替换 firebase-functions
包来定义函数?等等...
那么,我怎样才能将我的 Firebase 函数移到云端 运行 而不是 运行 进入 540 秒的最大超时限制?
const functions = require('firebase-functions');
const runtimeOpts = {timeoutSeconds: 540,memory: '2GB'}
exports.hourlyData = functions.runWith(runtimeOpts).pubsub.schedule('every 1 hours')
前言: 以下步骤已针对更广泛的受众进行推广,而不仅仅是 OP 的问题(涵盖 HTTP 事件、计划和 Pub/Sub 函数)并进行了调整来自问题中链接的文档:Deploying Node.JS Images on Cloud Run.
第 0 步:Code/Architecture审核
通常情况下,超过 9 分钟的 Cloud Function 超时是您代码中的错误造成的 - 请确保在切换到 Cloud 运行 之前对此进行评估,因为这只会使问题更糟。其中最常见的是顺序而不是并行异步处理(通常是由于在 for
/while
循环中使用 await
)。
如果您的代码正在执行需要很长时间的有意义的工作,请考虑将其拆分为可以并行处理输入数据的“子函数”。您可以使用单个函数触发一个函数的多个实例,而不是为数据库中的每个用户处理数据,每个实例处理不同的用户 ID 范围,例如 a-l\uf8ff
、m-z\uf8ff
、A-L\uf8ff
、M-Z\uf8ff
和 0-9\uf8ff
。
最后,Cloud 运行 和 Cloud Functions 非常相似,它们旨在接收请求、处理请求然后 return 响应。 Cloud Functions 的限制为 9 分钟,Cloud 运行s 的限制为 60 分钟。一旦该响应完成(因为服务器结束响应、客户端丢失连接或客户端中止请求),实例将被严重限制或终止。虽然您可以在使用 Cloud 运行 时使用 WebSockets 和 gRPC 在服务器和客户端之间进行持久通信,但它们仍然受到此限制。有关详细信息,请参阅 Cloud Run: General development tips 文档。
与其他无服务器解决方案一样,您的客户端和服务器需要能够处理与不同实例的连接。您的代码不应使用本地状态(例如 session 数据的本地存储)。有关详细信息,请参阅 Setting request timeout 文档。
第 1 步:安装 Google Cloud SDK
这一步我会推荐你参考Installing Google Cloud SDK documentation。
安装后,调用 gcloud auth login
并使用用于目标 Firebase 项目的帐户登录。
第 2 步:获取您的 Firebase 项目设置
打开你的project settings in the Firebase Console并记下你的项目ID和你的默认GCP资源位置。
Firebase Functions 和 Cloud 运行 实例应尽可能 co-located 与您的 GCP 资源。在 Firebase Functions 中,这是通过 changing the region in code 并使用 CLI 部署来实现的。对于 Cloud 运行,您可以在命令行中将这些参数指定为标志(或使用 Google Cloud Console)。为了下面的说明和简单起见,我将使用 us-central1
作为我的 默认 GCP 资源位置 是 nam5 (us-central)
.
如果在您的项目中使用 Firebase 实时数据库,请访问您的 RTDB settings in the Firebase Console 并记下您的 数据库 URL。这通常是 https://PROJECT_ID.firebaseio.com/
.
如果在您的项目中使用 Firebase 存储,请访问您的 Cloud Storage settings in the Firebase Console 并记下您的 Bucket URI。从这个 URI 中,我们需要注意主机(忽略 gs://
部分),它通常采用 PROJECT_ID.appspot.com
.
这里有一个 table,您可以复制它以帮助跟踪:
Project ID: | PROJECT_ID |
Database URL: | https://PROJECT_ID.firebaseio.com |
Storage Bucket: | PROJECT_ID.appspot.com |
Default GCP Resource Location: | |
Chosen Cloud Run Region: |
步骤 3:创建目录
在您的 Firebase 项目目录或您选择的目录中,创建一个新的 cloudrun
文件夹。
不同于 Firebase Cloud Functions,您可以在单个代码模块中定义多个函数,每个 Cloud 运行 图像使用自己的代码模块。因此,每个 Cloud 运行 图像都应存储在自己的目录中。
因为我们要定义一个名为 helloworld
的 Cloud 运行 实例,所以我们将在 cloudrun
.
helloworld
的目录
mkdir cloudrun
mkdir cloudrun/helloworld
cd cloudrun/helloworld
第 4 步:创建 package.json
为了正确部署 Cloud 运行 映像,我们需要提供一个 package.json
用于在部署的容器中安装依赖项。
package.json
文件的格式类似于:
{
"name": "SERVICE_NAME",
"description": "",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "node index.js"
"image": "gcloud builds submit --tag gcr.io/PROJECT_ID/SERVICE_NAME --project PROJECT_ID",
"deploy:public": "gcloud run deploy SERVICE_NAME --image gcr.io/PROJECT_ID/SERVICE_NAME --allow-unauthenticated --region REGION_ID --project PROJECT_ID",
"deploy:private": "gcloud run deploy SERVICE_NAME --image gcr.io/PROJECT_ID/SERVICE_NAME --no-allow-unauthenticated --region REGION_ID --project PROJECT_ID",
"describe": "gcloud run services describe SERVICE_NAME --region REGION_ID --project PROJECT_ID --platform managed",
"find": "gcloud run services describe SERVICE_NAME --region REGION_ID --project PROJECT_ID --platform managed --format='value(status.url)'"
},
"engines": {
"node": ">= 12.0.0"
},
"author": "You",
"license": "Apache-2.0",
"dependencies": {
"express": "^4.17.1",
"body-parser": "^1.19.0",
/* ... */
},
"devDependencies": {
/* ... */
}
}
在上面的文件中,SERVICE_NAME
、REGION_ID
和 PROJECT_ID
将根据步骤 2 中的详细信息进行适当换出。我们还安装了 express
和body-parser
处理传入的请求。
还有一些模块脚本可以帮助部署。
Script Name | Description |
---|---|
image |
Submits the image to Cloud Build to be added to the Container Registry for other commands. |
deploy:public |
Deploys the image from the above command to be used by Cloud Run (while allowing any requester to invoke it) and returns its service URL (which is partly randomized). |
deploy:private |
Deploys the image from the above command to be used by Cloud Run (while requiring that the requester that invokes it is an authorized user/service account) and returns its service URL (which is partly randomized). |
describe |
Gets the statistics & configuration of the deployed Cloud Run. |
find |
Extracts only the service URL from the response of npm run describe |
注意:这里的“授权用户”是指与项目关联的Google帐户,而不是普通的Firebase用户。要允许 Firebase 用户调用您的云 运行,您必须使用 deploy:public
部署它并在您的云 运行 代码中处理令牌验证,适当地拒绝请求。
作为此文件填写的示例,您会得到:
{
"name": "helloworld",
"description": "Simple hello world sample in Node with Firebase",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "node index.js"
"image": "gcloud builds submit --tag gcr.io/com-example-cloudrun/helloworld --project com-example-cloudrun",
"deploy:public": "gcloud run deploy helloworld --image gcr.io/com-example-cloudrun/helloworld --allow-unauthenticated --region us-central1 --project com-example-cloudrun",
"deploy:public": "gcloud run deploy helloworld --image gcr.io/com-example-cloudrun/helloworld --no-allow-unauthenticated --region us-central1 --project com-example-cloudrun",
"describe": "gcloud run services describe helloworld --region us-central1 --project com-example-cloudrun --platform managed",
"find": "gcloud run services describe helloworld --region us-central1 --project com-example-cloudrun --platform managed --format='value(status.url)'"
},
"engines": {
"node": ">= 12.0.0"
},
"author": "You",
"license": "Apache-2.0",
"dependencies": {
/* ... */
},
"devDependencies": {
/* ... */
}
}
第 5 步:创建容器文件
告诉 Cloud Build 为您的云使用什么容器运行 图片,您必须为您的图片创建一个 Dockerfile
。为防止向服务器发送错误的文件,您还应该指定一个 .dockerignore
文件。
在此文件中,我们使用第 2 步中的 Firebase 项目设置重新创建 process.env.FIREBASE_CONFIG
环境变量。此变量由 Firebase Admin SDK 使用并包含以下信息作为 JSON 字符串:
{
databaseURL: "https://PROJECT_ID.firebaseio.com",
storageBucket: "PROJECT_ID.appspot.com",
projectId: "PROJECT_ID"
}
这里是cloudrun/helloworld/Dockerfile
:
# Use the official lightweight Node.js 14 image.
# https://hub.docker.com/_/node
FROM node:14-slim
# Create and change to the app directory.
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./
# Install production dependencies.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN npm install --only=production
# Copy local code to the container image.
COPY . ./
# Define default configuration for Admin SDK
# databaseURL is usually "https://PROJECT_ID.firebaseio.com", but may be different.
# TODO: Update me
ENV FIREBASE_CONFIG={"databaseURL":"https://PROJECT_ID.firebaseio.com","storageBucket":"PROJECT_ID.appspot.com","projectId":"PROJECT_ID"}
# Run the web service on container startup.
CMD [ "node", "index.js" ]
这里是cloudrun/helloworld/.dockerignore
:
Dockerfile
.dockerignore
node_modules
npm-debug.log
第 6 步:创建和部署您的入口点
启动新的 Cloud 运行 实例时,它通常会使用 PORT
environment variable.
变体:迁移 HTTP 事件函数
当您使用 HTTP Event function from the firebase-functions
package, it internally handles body-parsing on your behalf. The Functions Framework uses the body-parser
package for this and defines the parsers here.
要处理用户授权,您可以使用这个 validateFirebaseIdToken()
中间件来检查随请求提供的 ID 令牌。
对于 HTTP-based 云 运行,需要配置 CORS 才能从浏览器调用它。这可以通过安装 cors
package 并适当配置来完成。在下面的示例中,cors
将反映发送给它的来源。
const express = require('express');
const cors = require('cors')({origin: true});
const app = express();
app.use(cors);
// To replicate a Cloud Function's body parsing, refer to
// https://github.com/GoogleCloudPlatform/functions-framework-nodejs/blob/d894b490dda7c5fd4690cac884fd9e41a08b6668/src/server.ts#L47-L95
app.use(/* body parsers */);
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
// Start of your handlers
app.get('/', (req, res) => {
const name = process.env.NAME || 'World';
res.send(`Hello ${name}!`);
});
// End of your handlers
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});
在$FIREBASE_PROJECT_DIR/cloudrun/helloworld
目录中,执行以下命令来部署你的镜像:
npm run image // builds container & stores to container repository
npm run deploy:public // deploys container image to Cloud Run
变体:使用 Cloud Scheduler 调用
使用 Cloud Scheduler 调用 Cloud 运行 时,您可以选择调用它的方法(GET
、POST
(默认)、PUT
, HEAD
, DELETE
).要复制 Cloud Functions 的 data
和 context
参数,最好使用 POST
,因为这些参数随后将在请求的 body 中传递。与 Firebase Functions 一样,这些来自 Cloud Scheduler 的请求可能会被重试,因此请确保 handle idempotency appropriately.
注意: 即使 Cloud Scheduler 调用请求的 body 是 JSON-formatted,该请求也会使用 Content-Type: text/plain
提供,这我们需要处理。
此代码改编自 Functions Framework source(Google LLC、Apache 2.0)
const express = require('express');
const { json } = require('body-parser');
async function handler(data, context) {
/* your logic here */
const name = process.env.NAME || 'World';
console.log(`Hello ${name}!`);
}
const app = express();
// Cloud Scheduler requests contain JSON using
"Content-Type: text/plain"
app.use(json({ type: '*/*' }));
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
app.post('/*', (req, res) => {
const event = req.body;
let data = event.data;
let context = event.context;
if (context === undefined) {
// Support legacy events and CloudEvents in structured content mode, with
// context properties represented as event top-level properties.
// Context is everything but data.
context = event;
// Clear the property before removing field so the data object
// is not deleted.
context.data = undefined;
delete context.data;
}
Promise.resolve()
.then(() => handler(data, context))
.then(
() => {
// finished without error
// the return value of `handler` is ignored because
// this isn't a callable function
res.sendStatus(204); // No content
},
(err) => {
// handler threw error
console.error(err.stack);
res.set('X-Google-Status', 'error');
// Send back the error's message (as calls to this endpoint
// are authenticated project users/service accounts)
res.send(err.message);
}
)
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});
注意: Functions Framework 通过发回带有 X-Google-Status: error
header 的 HTTP 200 OK
响应来处理错误。这实际上意味着“成功失败”。作为局外人,我不确定为什么要这样做,但我可以假设这样做是为了让调用者知道不必费心重试该函数 - 它只会得到相同的结果。
在$FIREBASE_PROJECT_DIR/cloudrun/helloworld
目录中,执行以下命令来部署你的镜像:
npm run image // builds container & stores to container repository
npm run deploy:private // deploys container image to Cloud Run
注意:在下面的设置命令中(只需要运行这一次),PROJECT_ID
,SERVICE_NAME
,SERVICE_URL
和 IAM_ACCOUNT
需要酌情替换。
接下来我们需要create a service account that Cloud Scheduler can use to invoke the Cloud Run. You can call it whatever you want such as scheduled-run-invoker
. The email of this service account will be referred to as IAM_ACCOUNT
in the next step. This Google Cloud Tech YouTube video (starts at the right spot, about 15s) will quickly show what you need to do. Once you've created the account, you can create the Cloud Scheduler job跟随视频的下一个 30 秒左右或使用以下命令:
gcloud scheduler jobs create http scheduled-run-SERVICE_NAME /
--schedule="every 1 hours" /
--uri SERVICE_URL /
--attempt-deadline 60m /
--http-method post /
--message-body='{"optional-custom-data":"here","if-you":"want"}' /
--oidc-service-account-email IAM_ACCOUNT
--project PROJECT_ID
您的云 运行 现在应该已安排。
变体:使用 Pub/Sub
调用据我了解,部署过程与计划的 运行 (deploy:private
) 相同,但我不确定具体细节。但是,这里是 Pub/Sub 解析器的 Cloud 运行 源代码:
此代码改编自 Functions Framework source(Google LLC、Apache 2.0)
const express = require('express');
const { json } = require('body-parser');
const PUBSUB_EVENT_TYPE = 'google.pubsub.topic.publish';
const PUBSUB_MESSAGE_TYPE =
'type.googleapis.com/google.pubsub.v1.PubsubMessage';
const PUBSUB_SERVICE = 'pubsub.googleapis.com';
/**
* Extract the Pub/Sub topic name from the HTTP request path.
* @param path the URL path of the http request
* @returns the Pub/Sub topic name if the path matches the expected format,
* null otherwise
*/
const extractPubSubTopic = (path: string): string | null => {
const parsedTopic = path.match(/projects\/[^/?]+\/topics\/[^/?]+/);
if (parsedTopic) {
return parsedTopic[0];
}
console.warn('Failed to extract the topic name from the URL path.');
console.warn(
"Configure your subscription's push endpoint to use the following path: ",
'projects/PROJECT_NAME/topics/TOPIC_NAME'
);
return null;
};
async function handler(message, context) {
/* your logic here */
const name = message.json.name || message.json || 'World';
console.log(`Hello ${name}!`);
}
const app = express();
// Cloud Scheduler requests contain JSON using
"Content-Type: text/plain"
app.use(json({ type: '*/*' }));
app.enable('trust proxy'); // To respect X-Forwarded-For header. (Cloud Run is behind a load balancer proxy)
app.disable('x-powered-by'); // Disables the 'x-powered-by' header added by express (best practice)
app.post('/*', (req, res) => {
const body = req.body;
if (!body) {
res.status(400).send('no Pub/Sub message received');
return;
}
if (typeof body !== "object" || body.message === undefined) {
res.status(400).send('invalid Pub/Sub message format');
return;
}
const context = {
eventId: body.message.messageId,
timestamp: body.message.publishTime || new Date().toISOString(),
eventType: PUBSUB_EVENT_TYPE,
resource: {
service: PUBSUB_SERVICE,
type: PUBSUB_MESSAGE_TYPE,
name: extractPubSubTopic(req.path),
},
};
// for storing parsed form of body.message.data
let _jsonData = undefined;
const data = {
'@type': PUBSUB_MESSAGE_TYPE,
data: body.message.data,
attributes: body.message.attributes || {},
get json() {
if (_jsonData === undefined) {
const decodedString = Buffer.from(base64encoded, 'base64')
.toString('utf8');
try {
_jsonData = JSON.parse(decodedString);
} catch (parseError) {
// fallback to raw string
_jsonData = decodedString;
}
}
return _jsonData;
}
};
Promise.resolve()
.then(() => handler(data, context))
.then(
() => {
// finished without error
// the return value of `handler` is ignored because
// this isn't a callable function
res.sendStatus(204); // No content
},
(err) => {
// handler threw error
console.error(err.stack);
res.set('X-Google-Status', 'error');
// Send back the error's message (as calls to this endpoint
// are authenticated project users/service accounts)
res.send(err.message);
}
)
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`helloworld: listening on port ${port}`);
});