
最近折腾 Kamal 部署,踩了一个大坑。我的 .kamal/secrets 文件里全是明文 API 密钥,放在笔记本上,谁拿到都能用。这对于要做 SOC 2 认证的项目来说简直是噩梦。
解决方案:用 Kamal 配合 AWS Secrets Manager(AWS密钥管理服务)存密钥,部署到 Hetzner 的 VPS(虚拟专用服务器)。明文密钥彻底消失,托管成本低,合规也过得去。
Kamal 部署应用很方便,但默认情况下密钥是存在明文文件里的。SOC 2 和 GDPR(通用数据保护条例)合规都不允许这样,必须用托管的密钥存储。我选了 AWS Secrets Manager。
但接下来遇到了另一个坑。kamal secrets fetch --adapter aws_secrets_manager 配合 --from 参数使用时,它期望每个密钥都是独立的 AWS Secret。如果你把所有密钥存成一个 JSON 对象(我就是这么干的),就会报错:
ERROR (RuntimeError): myapp/production/secrets//DEEPGRAM_API_KEY: Secrets Manager can't find the specified secret.
Hetzner CAX 系列最低每月 4 欧元左右。我用的是 CX22,2 核 CPU 加 4GB 内存,跑生产环境够用了。
# 在 Hetzner 服务器上
apt update && apt install -y docker.io # 复制 SSH 密钥,这样 Kamal 才能连上服务器
ssh-copy-id root@你的服务器IP
config/deploy.yml 配置:
servers: web: hosts: - runtime.yourdomain.com proxy: ssl: true hosts: - runtime.yourdomain.com healthcheck: path: /health/ready registry: server: docker.io username: your-docker-user password: - KAMAL_REGISTRY_PASSWORD
你需要一个 Docker Hub 账号和一个个人访问令牌作为 KAMAL_REGISTRY_PASSWORD。
在 AWS Secrets Manager 控制台操作:
{ "DEEPGRAM_API_KEY": "your_deepgram_key", "ASSEMBLY_AI_API_KEY": "your_assemblyai_key", "REDIS_URL": "redis://:password@your-redis:6379", "KAMAL_REGISTRY_PASSWORD": "your_docker_token"
}
myapp/production/secrets选离服务器近的区域。如果 Hetzner 机器在德国,就用 eu-central-1(法兰克福),延迟低,合规也省心。
笔记本需要在部署时读取密钥,得给它配权限。
kamal-deploysecrets-manager 的组,绑定 SecretsManagerReadWrite 策略{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:BatchGetSecretValue", "secretsmanager:ListSecrets" ], "Resource": "*" } ]
}
IAM 策略需要一点时间传播。刚创建完如果报权限错误,等 30 秒再试。
aws configure
# AWS Access Key ID: 粘贴 IAM 用户的访问密钥 ID
# AWS Secret Access Key: 粘贴
# Default region name: eu-central-1
# Default output format: json
测试一下:
aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text | head -c 50
能看到 JSON 的开头就说明配置成功了。
这里我卡了很久。--from 参数要求每个密钥对应一个独立的 AWS Secret,如果要存 20 个密钥就得建 20 个 Secret,太麻烦了。
我的解决方案是用 AWS CLI 配合 Python 提取。每个配置行都是独立的:
# AWS Secrets Manager: myapp/production/secrets (eu-central-1)
DEEPGRAM_API_KEY=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['DEEPGRAM_API_KEY'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
ASSEMBLY_AI_API_KEY=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['ASSEMBLY_AI_API_KEY'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
REDIS_URL=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['REDIS_URL'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
KAMAL_REGISTRY_PASSWORD=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['KAMAL_REGISTRY_PASSWORD'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
每一行都拉取完整 JSON 然后提取一个字段。Kamal 在独立子 shell 中执行每行,所以行之间没有共享变量。这套方案可行。
如果你习惯用 jq,也可以:
DEEPGRAM_API_KEY=$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text | jq -r '.DEEPGRAM_API_KEY')
kamal deploy
Kamal 在部署时从 AWS 拉取密钥,注入到容器里。服务器上永远不会出现明文密钥文件。
我给每个环境配一个独立的 AWS Secret。两个环境都从 AWS 拉取,没有明文密钥残留。
# .kamal/secrets (kamal deploy 使用)
DEEPGRAM_API_KEY=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['DEEPGRAM_API_KEY'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)")
KAMAL_REGISTRY_PASSWORD=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['KAMAL_REGISTRY_PASSWORD'])" "$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text)") # .kamal/secrets.staging (kamal deploy -d staging 使用)
DEEPGRAM_API_KEY=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['DEEPGRAM_API_KEY'])" "$(aws secretsmanager get-secret-value --secret-id myapp/staging/secrets --query SecretString --output text)")
KAMAL_REGISTRY_PASSWORD=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['KAMAL_REGISTRY_PASSWORD'])" "$(aws secretsmanager get-secret-value --secret-id myapp/staging/secrets --query SecretString --output text)")
两个文件只是密钥名称不同,生产用 myapp/production/secrets,预发布用 myapp/staging/secrets。运行 kamal deploy -d staging 就从预发布配置文件读取。预发布密钥同样存在 AWS 里,明文什么的不存在。这点对于 SOC 2 认证很重要,审计人员会检查每一个环境的配置。
明文密钥没了,SOC 2 和 GDPR 合规通过,Hetzner 月账单始终不超过 5 欧元。
感谢 AWS 文档团队、Kamal 维护者和 Hetzner 让托管成本保持友好。希望这篇文章能让你少踩几个我踩过的坑。现在回去写代码了。