正在加载,请稍候…

环境变量与机密管理:完整生产指南

学习如何在开发、预发布和生产环境中正确管理环境变量和机密,涵盖 .env 文件、保险库方案、Docker 机密、Kubernetes 机密以及常见错误。

环境变量与机密管理:完整生产指南

硬编码机密的问题

每年,成千上万的凭据因有人在源代码中直接硬编码数据库密码、API 密钥或私钥而泄露到 GitHub。修复方法听起来很简单——"只需使用环境变量"——但在多个环境和团队成员之间的实际实现才是大多数项目出错的地方。

本指南涵盖从基本的 .env 文件到生产级机密管理的所有内容。

环境变量与机密管理:完整生产指南插图

十二要素应用:配置原则

十二要素方法论指出:"将配置存储在环境中。" 任何在不同部署(开发/预发布/生产)之间变化的内容都应属于环境变量:

  • 数据库连接字符串
  • API 密钥和令牌
  • OAuth 客户端密钥
  • 第三方服务凭据
  • 功能开关
  • 端口号和主机名

在不同环境之间不变的代码(业务逻辑、算法)属于代码仓库。

.env 文件:开发基础

# .env(仅开发环境——切勿提交到 git)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379
API_KEY=dev-key-not-real
JWT_SECRET=development-secret-change-in-production
STRIPE_SECRET_KEY=sk_test_...

# .env.example(始终提交——显示需要哪些变量)
NODE_ENV=
PORT=3000
DATABASE_URL=
REDIS_URL=
API_KEY=
JWT_SECRET=
STRIPE_SECRET_KEY=
# .gitignore——绝对必要
.env
.env.local
.env.*.local
.env.production
# 但不要忽略 .env.example

在 Node.js 中加载 .env

// 使用 dotenv(最常见)
import 'dotenv/config'; // ES 模块——自动加载 .env

// 或手动:
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' }); // 指定自定义路径

// 访问变量
const dbUrl = process.env.DATABASE_URL;
const port = parseInt(process.env.PORT ?? '3000', 10);

验证环境变量(关键!)

// 使用 zod 验证和类型化环境变量
import { z } from 'zod';

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'staging', 'production']),
  PORT: z.string().regex(/^d+$/).transform(Number).default('3000'),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  REDIS_URL: z.string().url().optional(),
});

// 启动时解析并验证——配置错误时快速失败
const env = envSchema.safeParse(process.env);

if (!env.success) {
  console.error('❌ 无效的环境变量:');
  console.error(env.error.flatten().fieldErrors);
  process.exit(1);
}

// 导出类型化配置
export const config = env.data;
// 现在:config.PORT 是 number,config.DATABASE_URL 是 string,等等

启动时快速失败远好于在首次访问缺失变量时出现神秘的运行时错误。

环境变量与机密管理:完整生产指南插图

环境特定文件

不同框架处理多个环境的方式不同:

# 常见约定(Vite、Create React App、Next.js)
.env                # 基础默认值(所有环境)
.env.local          # 本地覆盖(已 gitignore)
.env.development    # 开发环境特定
.env.test           # 测试环境特定
.env.production     # 生产环境(已提交但无机密!)

# 加载顺序(Vite):
# .env → .env.[mode] → .env.local → .env.[mode].local
# 后面的文件覆盖前面的
# 在 package.json 脚本中:
{
  "scripts": {
    "dev": "NODE_ENV=development node server.js",
    "test": "NODE_ENV=test jest",
    "start": "NODE_ENV=production node server.js"
  }
}

Docker:环境变量

# Dockerfile——切勿硬编码机密
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .

# ✅ 引用带默认值的环境变量
ENV NODE_ENV=production
ENV PORT=3000
# 不要在此设置机密——在运行时传递

EXPOSE $PORT
CMD ["node", "server.js"]
# docker-compose.yml(开发环境)
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - PORT=3000
      # 从主机环境引用:
      - DATABASE_URL=${DATABASE_URL}
      - JWT_SECRET=${JWT_SECRET}
    env_file:
      - .env  # 加载整个 .env 文件

Docker 机密(Swarm/生产环境)

# 创建机密
echo "my-super-secret-password" | docker secret create db_password -

# 在 Swarm 的 docker-compose 中:
services:
  app:
    image: myapp
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    external: true

# 在应用中,读取机密文件:
const dbPassword = fs.readFileSync(process.env.DB_PASSWORD_FILE, 'utf8').trim();

Kubernetes 机密

# 创建机密(base64 编码)
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:  # stringData 自动 base64 编码
  database-url: "postgresql://user:pass@postgres:5432/myapp"
  jwt-secret: "your-production-jwt-secret"
  stripe-key: "sk_live_..."
---
# 在 Deployment 中引用
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: app
          envFrom:
            - secretRef:
                name: app-secrets
          # 或单个环境变量:
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: app-secrets
                  key: database-url
# 从命令行创建机密(推荐——避免包含机密的 YAML 文件)
kubectl create secret generic app-secrets   --from-literal=database-url="postgresql://..."   --from-literal=jwt-secret="$(openssl rand -hex 32)"

# 查看机密(base64 编码)
kubectl get secret app-secrets -o yaml

# 解码特定值
kubectl get secret app-secrets -o jsonpath='{.data.jwt-secret}' | base64 -d

环境变量与机密管理:完整生产指南插图

HashiCorp Vault:企业级机密管理

对于大型团队和企业,Vault 提供带有审计追踪的集中式机密管理:

# 启动 Vault 开发服务器(仅开发环境)
vault server -dev

export VAULT_ADDR='http://127.0.0.1:8200'
export VAULT_TOKEN='dev-only-token'

# 存储机密
vault kv put secret/myapp   database_url="postgresql://..."   jwt_secret="$(openssl rand -hex 32)"

# 读取机密
vault kv get secret/myapp
vault kv get -field=jwt_secret secret/myapp
// 在 Node.js 中从 Vault 读取
import vault from 'node-vault';

const client = vault({
  apiVersion: 'v1',
  endpoint: process.env.VAULT_ADDR,
  token: process.env.VAULT_TOKEN,  // 或使用 AppRole/K8s 认证
});

const { data } = await client.read('secret/data/myapp');
const { database_url, jwt_secret } = data.data;

GitHub Actions / CI 机密

# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
          STRIPE_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
        run: |
          # 这些变量在日志中被屏蔽
          npm run deploy

在以下位置设置机密:仓库 → 设置 → 机密和变量 → Actions

云提供商机密管理器

// AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

const client = new SecretsManagerClient({ region: 'us-east-1' });

async function getSecret(secretName) {
  const response = await client.send(
    new GetSecretValueCommand({ SecretId: secretName })
  );
  return JSON.parse(response.SecretString);
}

const { database_url, jwt_secret } = await getSecret('myapp/production');
// Google Cloud Secret Manager
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';

const client = new SecretManagerServiceClient();

async function getSecret(name) {
  const [version] = await client.accessSecretVersion({
    name: `projects/MY_PROJECT/secrets/${name}/versions/latest`
  });
  return version.payload.data.toString();
}

安全检查清单

# 审计仓库中泄露的机密
git log --all --full-history -- "*.env"
git grep -i "password|secret|key|token" -- "*.ts" "*.js" "*.json"

# 使用 git-secrets 防止提交机密
brew install git-secrets
git secrets --install
git secrets --register-aws

# 检测现有代码库中的机密
pip install detect-secrets
detect-secrets scan --baseline .secrets.baseline

# 如果发现泄露,轮换机密:
# 1. 立即撤销受损凭据
# 2. 生成新凭据
# 3. 更新所有部署
# 4. 审计日志以查找未授权访问
# 5. 考虑重写 git 历史(git-filter-repo)

总结:每个环境的正确工具

环境 解决方案
本地开发 .env 文件(已 gitignore)
CI/CD 平台机密(GitHub Actions、GitLab CI)
Docker 开发 docker-compose 中的 env_file
Docker 生产 Docker Secrets 或挂载卷
Kubernetes Kubernetes Secrets + Sealed Secrets
AWS Secrets Manager / GCP Secret Manager
企业 HashiCorp Vault

→ 使用 Token Generator 生成安全的随机机密。