云函数 / 云托管的密钥与环境变量分层管理
一句话定义:把云函数 / 云托管 / 前端三层的环境变量用 dev / staging / prod 三套配置隔开,本地走
.env+.gitignore、CI 走tcb login --apiKeyId/--apiKey注入永久密钥 + 流水线变量、生产走控制台环境变量,确保任何一层 secret 都不会进入 git 历史或前端 bundle。预计耗时:30 分钟 | 难度:中级
适用场景
- 已经部署了云函数 / 云托管服务,里面有 LLM API key、数据库密码、第三方服 务凭证
- 想把团队从"本地
.env散乱、生产环境依赖人工维护控制台"升级到一套规范流程 - 准备接 CI 流水线(工蜂 / 腾讯 CODING / GitHub Actions),需要让流水线能拿到密钥但又不让密钥进仓库
不适用:
- 完全个人项目,团队就一个人,且没有合规要求——那
.env+.gitignore就够了,不用上 CI - 用了第三方专门的 secret manager(HashiCorp Vault、AWS Secrets Manager),那有自己的整套规范
不该做的事
先列清楚什么不能做,比怎么做更重要:
| 反模式 | 为什么不行 |
|---|---|
| 把 secret 写进代码里 | git 历史里永远删不掉,rotate 一次也救不回;commit 内网仓库一样有风险 |
把 secret 写进 package.json 或 package-lock.json | 同上,且这些文件通常都会被提交 |
在 .env.example 里留真实值当"参考" | 经常有人 cp 完忘了改,真值进了 git |
| 微信小程序 / Web 前端代码里放 API key | 前端代码用户本地都能拿到,反编译/F12 一看就漏;前端任何变量都视作公开 |
用 console.log(process.env.OPENAI_KEY) 调试,提交时没删 | 日志会进监控系统,谁有日志权限谁有 key |
| 给整个团队共用同一个生产 key | 一旦某人离职或离场,整套 key 都要 rotate |
| 在 commit message / PR description 里贴 key 说"修了下这个" | review 一搜 sk- 就能挖出来 |
把 .env 提交到私有仓库觉得"反正没公开" | 团队成员变动、仓库迁移、备份外泄都是入口;私有不等于安全 |
三层环境的分工
┌─────────────────────┐
│ 本地开发(dev) │ .env(本地) + .gitignore + .env.example(模板)
└─────────────────────┘
↓
┌─────────────────────┐
│ CI 流水线 │ 流水线变量(工蜂/CODING) + tcb login --apiKeyId/--apiKey
└─────────────────────┘
↓
┌─────────────────────┐
│ 生产(prod) │ CloudBase 控制台「环境变量」
└─────────────────────┘
下面分层讲。
第一层:本地开发
.env + .gitignore
项目根目录建:
# 本地开发配置(永远不提交)
touch .env
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
echo "*.env" >> .gitignore
.env 内容(具体 key 名按你的项目):
# 本地开发用的 LLM 网关代理 token
PROXY_ACCESS_TOKEN=local-dev-token-xxxxxxxxxxxxxxxx
# 上游 LLM 真 key
UPSTREAM_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
# 本地开发的 PostgreSQL 连接(本机 docker 起的实例)
PG_HOST=127.0.0.1
PG_PASSWORD=local-pg-password
提交一份模板给团队成员复用,不写真值:
touch .env.example
.env.example:
# 复制本文件为 .env 并填入真实值
PROXY_ACCESS_TOKEN=
UPSTREAM_API_KEY=
PG_HOST=
PG_PASSWORD=
.env.example 是允许提交的——它只是文件骨架,不含值。新成员 clone 仓库后第一件事是 cp .env.example .env,然后去找内部文档填值。
Node.js 代码里读 .env
云函数和云托管的 Node 项目里装 dotenv:
npm install --save-dev dotenv
入口文件最顶部:
// 仅本地开发加载 .env;部署到云函数后,环境变量由平台注入,不需要 dotenv
if (process.env.NODE_ENV !== 'production') {
require('dotenv').config();
}
const apiKey = process.env.UPSTREAM_API_KEY;
部署到 CloudBase 后 NODE_ENV=production 是平台默认,不会再尝试读 .env——这一点重要,避免误把 .env 一起打包上去后被加载。
.gitignore 检查清单
提交前一定看一眼 .gitignore 至少包含这些(按需增减):
# 环境变量
.env
.env.*
!.env.example
*.env
# CloudBase CLI 缓存
.cloudbase/
cloudbaserc.json.local
# 各种自动生成的密钥文件
tcb_custom_login.json
*.pem
*.key
firebase-adminsdk-*.json
service-account*.json
!.env.example 是允许 example 模板进 git 的例外。
第二层:CI 流水线
CI 不能拿本地 .env,因为 CI 跑在另一台机器上,且仓库里没有 .env。要走流水线变量。
配置流水线变量
以工蜂(CODING / GitLab 同理)为例:
- 仓库 → 「设置 → 流水线 → 流水线变量」
- 加变量,例如
TCB_DEPLOY_TOKEN、PROD_UPSTREAM_API_KEY、PROD_PG_PASSWORD - 勾「保护」+「掩码」(前者限制只在受保护分支生效,后者让日志里这些值显示成
***)
CloudBase CLI 在 CI 里的非交互登录
CloudBase CLI 在 CI 里不能用浏览器登录(没人按确认按钮)。改用 tcb login --apiKeyId/--apiKey 显式注入腾讯云永久密钥(区别于本地 tcb login 的浏览器登录):
# 1. 用永久密钥非交互登录
tcb login --apiKeyId $TENCENT_SECRET_ID --apiKey $TENCENT_SECRET_KEY
# 2. 部署
tcb fn deploy llm-proxy --httpFn -e your-env-id
TENCENT_SECRET_ID / TENCENT_SECRET_KEY 在腾讯云访问管理 CAM 控制台 创建,仅用于 CI 部署,不要在开发者本地使用(开发者本地用 tcb login 浏览器登录拿临时密钥,遵循最小权限)。
CI 步骤里绝对不要 echo $TENCENT_SECRET_KEY——一旦日志被外人看到等于 key 公开。流水线掩码只是兜底。
在 CI 里同时下发应用层 secret 到云函数
部署的同时把 UPSTREAM_API_KEY 等业务密钥灌到云函数环境变量,可以分两步:
# 1. 用永久密钥登录 CLI
tcb login --apiKeyId $TENCENT_SECRET_ID --apiKey $TENCENT_SECRET_KEY
# 2. 部署函数代码
tcb fn deploy llm-proxy --httpFn -e your-env-id
# 3. 通过云开发 OpenAPI / @cloudbase/manager-node 更新环境变量
# (具体接口请参考 manager-node 的 functions.updateFunctionConfig)
或者把环境变量写到 cloudbaserc.json 的 functions.envVariables 里,部署时一并更新。不要把 cloudbaserc.json 里的 secret 字段提交到 git,可以用 cloudbaserc.local.json + .gitignore,或在 CI 里用 envsubst / sed 把占位符替换成真值再部署。
具体命令以 @cloudbase/cli 的当前版本为准——CLI 的环境变量管理子命令各版本略有差异,参考 tcb fn --help 实际选项。
第三层:生产环境
生产 secret 的最终落点是 CloudBase 控制台环境变量:
| 资源 | 配置入口 |
|---|---|
| 云函数(事件 / Web 函数) | 控制台 → 云函数 → 函数详情 → 函数配置 → 环境变量 |
| 云托管服务 | 控制台 → 云托管 → 服务详情 → 服务设置 → 版本管理 → 新版本时配置 |
云托管和云函数的环境变量机制略有差异:
| 维度 | 云函数 | 云托管 |
|---|---|---|
| 改完是否立即生效 | 重新调用即生效(实例热更新) | 需要发布新版本,并切流量 |
| 是否绑定版本 | 不绑定(环境变量是函数级) | 绑定服务版本,每个版本独立 |
| Dockerfile 默认值 | 无此概念 | ENV KEY=VALUE 设置默认,控制台同名变量优先生效 |
跨环境(dev / staging / prod)命名约定
如果生产、预发、开发各占一个 CloudBase 环境,每个环境里同名变量直接区分即可(同一个 key 名 UPSTREAM_API_KEY 在 prod 和 staging 是两个不同的值)。
如果三套环境塞在同一个 CloudBase 环境里(不推荐但常见),用前缀区分:
PROD_UPSTREAM_API_KEY=...
STAGING_UPSTREAM_API_KEY=...
DEV_UPSTREAM_API_KEY=...
代码里按 process.env.STAGE 选:
const stage = process.env.STAGE || 'dev';
const apiKey = process.env[`${stage.toUpperCase()}_UPSTREAM_API_KEY`];
建议直接用三套环境,资源隔离永远比命名约定可靠。
前端 secret 的特别提醒
任何前端代码里出现的环境变量都不是 secret,包括:
- Next.js 的
NEXT_PUBLIC_* - Vite 的
VITE_* - 微信小程序里的任何配置项
它们 build 时被静态嵌入到 JS bundle,浏览器或微信开发者工具能直接看到。
正确做法:让前端只持有一个公开 endpoint URL,secret 留在云函数 / 云托管后端。例如:
✗ 错误:小程序代码里 const OPENAI_KEY = 'sk-xxx';
✓ 正确:小程序代码里 const PROXY_URL = 'https://your-env.service.tcloudbase.com/llm-proxy';
OPENAI_KEY 留在云函数环境变量里
如果你已经在前端硬编码过 key,那个 key 就视为已泄露,无论之后怎么改代码都不安全——必须 rotate。
key 已经泄露怎么办
每个服务 rotate 流程不一样,先列最常用的:
OpenAI
- 登录 platform.openai.com → API Keys
- 找到泄露的 key,点 Revoke(立即作废,所有调用 401)
- Create new secret key 生成新值
- 更新 CloudBase 控制台所有用到该 key 的云函数 / 云托管环境变量
- 检查账单页面,看泄露期间是否有异常调用(异常用量是关键线索)
腾讯云 CAM 永久密钥
- CAM 控制台 → 访问密钥
- 找到泄露的 SecretId,点「禁用」,再删除
- 新建一对 SecretId / SecretKey
- 更新 CI 流水线变量
- 立刻去操作记录查泄露期间是否有异常 API 调用
微信小程序 AppSecret
- 微信公众平台 → 开发设置 → 重置 AppSecret
- 重置后旧 AppSecret 立即失效(不像 OpenAI 有过渡期,这里是硬切)
- 立即更新所有用到的云函数环境变量并重新部署,否则线上立刻挂
数据库密码
- DMC 工具登录 → 账号管理 → 重置密码
- 更新所有用到的云函数 / 云托管环境变量
- 旧 session 不会立即断,但新连接拿不到了
每次 rotate 后做一件事:把"泄露的 key 怎么进 git 的"复盘下来。是 commit 漏检查?是某人本地 alias 把 .env 错当成 .env.example?是 CI 日志没打码?根因比 rotate 更重要,否则下次还会发生。
落地清单
部署到生产前过一遍:
-
.gitignore包含.env、.env.*(除.env.example)、tcb_custom_login.json、*.pem - 仓库历史里搜过
sk-、AKID、-----BEGIN PRIVATE,确认无 secret 残留(git log -S "sk-" -- '*.js') - CI 用永久密钥 + 掩码,开发本地用
tcb loginWeb 登录 - 前端代码里没有任何 API key(grep 一遍
sk-BearerapiKey:) - 所有云函数 / 云托管环境变量都从控制台或 CI 注入,不在代码里
- 团队有共享文档说明每个变量在哪个 CAM 子账号下、谁负责 rotate
- 关键 key(数据库 / 支付)开启了告警,异常调用量能触发通知
常见错误
| 现象 | 原因 | 修复 |
|---|---|---|
部署后 process.env.X 是 undefined | 控制台环境变量没加,或函数没重新部署 | 控制台加变量后重新触发部署(云函数会拉新值,但保险起见重新部署) |
改了 NEXT_PUBLIC_* 重启服务后前端拿到的还是旧值 | 这类变量是 build 期注入,重启不会更新 | 重新 npm run build 并部署 |
.env 不小心进了 git | .gitignore 漏配置 | git rm --cached .env,加 .gitignore,然后rotate 所有泄露的 key(git 历史里仍然能看到,加 ignore 不能洗白) |
CI 里日志显示 *** 但部署后函数报 401 | CI 把变量名拼错,掩码后看不出来 | CI 步骤里加 `printenv |
cloudbaserc.json 里写了 secret 占位符忘了替换 | 部署时占位符直接进了云函数环境变量 | 部署前在 CI 里加 grep 检查 \${.*} 这种未替换标记 |
| 重置 AppSecret 后线上小程序登录全挂 | 没同步更新云函数环境变量 | 流程里硬性要求"先更新环境变量再重置 AppSecret" |
| 同一个 key 在 staging 和 prod 混用 | 没做环境隔离 | 严肃做的话开三个 CloudBase 环境,不同 key |
相关文档
- 云函数环境变量 — 云函数环境变量配置和限制
- 云托管环境变量 — 云托管服务的环境变量
- CloudBase CLI 临时密钥 —
tcb secrets get在本地脚本初始化 SDK 的 用法 - CloudBase CLI 安装与登录 —
tcb login/ 登录方式 - 腾讯云 CAM 访问密钥 — 永久密钥的创建和回收
下一步
把这套基线立起来后,建议接着做:
connect-openai-api-cloud-function— LLM key 隔离的最佳实践范本,本篇里讲的所有规则都能套用deploy-nextjs-to-cloudbase-run— Next.js 应用的环境变量分层(NEXT_PUBLIC_*vs 服务端)add-rag-with-pgvector-cloudbase— 数据库密码 + LLM key 同时管理的复合场景