site logo

Marico's space

在 AWS Secrets Manager 中存储 Kamal 密钥并部署到廉价的 Hetzner VPS

AI技术与应用 2026-05-24 09:04:53 6

最近折腾 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 VPS

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 创建密钥

在 AWS Secrets Manager 控制台操作:

  1. 进入 Secrets Manager > 存储新密钥
  2. 选择"其他类型的密钥"
  3. 切换到纯文本标签页,粘贴你的 JSON
{ "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"
}
  1. 命名为 myapp/production/secrets
  2. 点击存储

选离服务器近的区域。如果 Hetzner 机器在德国,就用 eu-central-1(法兰克福),延迟低,合规也省心。

第三步:为笔记本创建 IAM 用户

笔记本需要在部署时读取密钥,得给它配权限。

  1. 进入 IAM > 用户 > 创建用户
  2. 用户名设为 kamal-deploy
  3. 取消控制台访问(只用 CLI)
  4. 创建一个叫 secrets-manager 的组,绑定 SecretsManagerReadWrite 策略
  5. 添加一条行内策略用于批量读取:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret", "secretsmanager:BatchGetSecretValue", "secretsmanager:ListSecrets" ], "Resource": "*" } ]
}
  1. 把用户加入组

IAM 策略需要一点时间传播。刚创建完如果报权限错误,等 30 秒再试。

第四步:配置 AWS CLI

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 的开头就说明配置成功了。

第五步:格式化 .kamal/secrets 文件

这里我卡了很久。--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 让托管成本保持友好。希望这篇文章能让你少踩几个我踩过的坑。现在回去写代码了。

原文链接:https://dev.to/ajcwebdev/storing-kamal-secrets-in-aws-secrets-manager-and-deploying-to-a-cheap-hetzner-vps-2m71