Next.js 13.5.6部署GAE时Datastore连接权限错误排查
我有一个使用App Router的Next.js 13.5.6项目,部署在Google App Engine(Standard)环境,采用Google Firestore in Datastore mode存储数据。本地Windows环境构建运行一切正常,但部署至GAE时触发权限错误,具体表现为/robots.txt等路由预渲染时无法访问Datastore,报错PERMISSION_DENIED: Missing or insufficient permissions。
错误日志
Beginning deployment of service [default]... ╔════════════════════════════════════════════════════════════╗ ╠═ Uploading 62 files to Google Cloud Storage ═╣ ╚════════════════════════════════════════════════════════════╝ File upload done. Updating service [default]...failed. ERROR: (gcloud.app.deploy) Error Response: [9] Cloud build [hash] status: FAILURE > myproject@3.1.0 build > next build Attention: Next.js now collects completely anonymous telemetry regarding usage. This information is used to shape Next.js' roadmap and prioritize features. You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL: https://nextjs.org/telemetry Creating an optimized production build ... ✓ Compiled successfully Linting and checking validity of types ... Collecting page data ... Generating static pages (0/19) ... Generating static pages (4/19) Generating static pages (9/19) Generating static pages (14/19) Error occurred prerendering page "/robots.txt". Read more: https://nextjs.org/docs/messages/prerender-error Error: 7 PERMISSION_DENIED: Missing or insufficient permissions. at callErrorFromStatus (/workspace/node_modules/@grpc/grpc-js/build/src/call.js:31:19) at Object.onReceiveStatus (/workspace/node_modules/@grpc/grpc-js/build/src/client.js:192:76) at Object.onReceiveStatus (/workspace/node_modules/@grpc/grpc-js/build/src/client-interceptors.js:360:141) at Object.onReceiveStatus (/workspace/node_modules/@grpc/grpc-js/build/src/client-interceptors.js:323:181) at /workspace/node_modules/@grpc/grpc-js/build/src/resolving-call.js:99:78 at process.processTicksAndRejections (node:internal/process/task_queues:77:11) for call at at ServiceClientImpl.makeUnaryRequest (/workspace/node_modules/@grpc/grpc-js/build/src/client.js:160:32) at ServiceClientImpl.<anonymous> (/workspace/node_modules/@grpc/grpc-js/build/src/make-client.js:105:19) at /workspace/node_modules/@google-cloud/datastore/build/src/v1/datastore_client.js:224:29 at /workspace/node_modules/google-gax/build/src/normalCalls/timeout.js:44:16 at repeat (/workspace/node_modules/google-gax/build/src/normalCalls/retries.js:80:25) at /workspace/node_modules/google-gax/build/src/normalCalls/retries.js:118:13 at OngoingCall.call (/workspace/node_modules/google-gax/build/src/call.js:67:27) at NormalApiCaller.call (/workspace/node_modules/google-gax/build/src/normalCalls/normalApiCaller.js:34:19) at /workspace/node_modules/google-gax/build/src/createApiCall.js:84:30 at process.processTicksAndRejections (node:internal/process/task_queues:95:5) ✓ Generating static pages (19/19) > Export encountered errors on following paths: /robots.txt/route: /robots.txt Full build logs: https://console.cloud.google.com/cloud-build/builds;region=[region]/[hash]?project=[project-id]
相关代码与配置
package.json
{ "name": "myproject", "version": "3.1.0", "description": "...description...", "private": true, "author": "Ivan Melnyk", "main": "next start", "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "deploy": "gcloud app deploy" }, "dependencies": { "@google-cloud/datastore": "^8.2.1", "cookies-next": "^4.0.0", "date-fns": "^2.30.0", "next": "^13.5.6", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^4.11.0", "server-only": "^0.0.1", "sharp": "^0.32.6" }, "devDependencies": { "@types/node": "20.6.5", "@types/react": "18.2.22", "autoprefixer": "^10.4.16", "cssnano": "^6.0.1", "eslint-config-next": "^13.5.3", "eslint-config-prettier": "^9.0.0", "postcss": "^8.4.30", "sass": "^1.68.0", "tailwindcss": "^3.3.3", "typescript": "5.2.2" } }
robots.ts
import { General } from 'data' export default async function robots() { const data = await General() return { rules: { userAgent: '*', allow: '/', disallow: '/email', }, sitemap: `${data.url}/sitemap.xml`, } }
data.ts(General函数)
import 'server-only' import { Datastore } from '@google-cloud/datastore' export async function General() { const datastore = new Datastore() const [[records]] = await datastore.runQuery(datastore.createQuery('general')) return records }
原因分析
本地运行正常是因为你使用的是gcloud auth登录的个人账号,该账号拥有Datastore的访问权限;而部署时的构建过程由Cloud Build执行,它使用的是独立的服务账号(格式为[PROJECT_NUMBER]@cloudbuild.gserviceaccount.com),这个账号默认没有Datastore的读写权限,因此在构建阶段预渲染路由时调用Datastore会触发权限错误。
注意:GAE运行时的默认服务账号([PROJECT_ID]@appspot.gserviceaccount.com)确实拥有Datastore权限,但这是在应用运行阶段生效,和构建阶段的Cloud Build服务账号不是同一个。
解决方案
方案1:给Cloud Build服务账号添加Datastore权限
- 打开Google Cloud控制台,进入IAM与管理员 > IAM页面
- 搜索Cloud Build的服务账号:格式为
[你的项目编号]@cloudbuild.gserviceaccount.com - 点击该账号右侧的编辑按钮,添加Cloud Datastore Viewer(仅读取,足够你的场景)或Cloud Datastore User角色
- 保存后重新执行部署命令
方案2:修改路由为动态生成,避免构建时预渲染
如果不想调整权限,可以修改robots.ts等路由,强制Next.js在运行时生成内容,而不是构建阶段预渲染:
import { General } from 'data' // 强制动态生成,避免构建时预渲染 export const dynamic = 'force-dynamic' export default async function robots() { const data = await General() return { rules: { userAgent: '*', allow: '/', disallow: '/email', }, sitemap: `${data.url}/sitemap.xml`, } }
这样路由会在GAE运行时处理,此时使用的是GAE的默认服务账号,拥有Datastore权限,就能正常访问数据。
内容的提问来源于stack exchange,提问作者Ivan Melnyk

