카마르와 문제가 발생했습니다. 제 .kamal/secrets 파일에는 노출된 API 키들이 제 노트북에 있었습니다. 접근할 수 있는 누구나 그것들을 읽을 수 있었습니다.
TLDR; 카마르를 AWS Secrets Manager와 함께 사용하고 Hetzner VPS에 배포하세요. 평문 비밀이 없고 저렴한 호스팅, 규정 준수에 만족합니다.
문제
카마ル은 애플리케이션 배포에 매우 좋습니다. 하지만 기본적으로 비밀번호는 평문 파일에 저장됩니다. SOC 2와 GDPR에 그렇게 하면 안 됩니다. 관리형 저장소가 필요합니다. 저는 AWS Secrets Manager를 선택했습니다.
그런데 그 다음에 다른 문제에 부딪혔습니다. kamal secrets fetch --adapter aws_secrets_manager 명령어는 --from 각 키가 자신만의 AWS 비밀번호여야 합니다. 모든 것을 JSON 덩어리로 저장하면 (저는 그렇게 했지만), 다음과 같은 결과가 나옵니다:
ERROR (RuntimeError): myapp/production/secrets//DEEPGRAM_API_KEY: Secrets Manager can't find the specified secret.
단계 1: Hetzner VPS
Hetzner CAX 시리즈는 월 약 4 유로부터 시작합니다. CX22를 사용하며, 2개의 vCPU와 4GB RAM을 사용합니다. 생산에 충분합니다.
# On your Hetzner server
apt update && apt install -y docker.io
# Copy your SSH key so Kamal can connect
ssh-copy-id root@your-server-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.
단계 2: AWS에서 비밀 정보 생성
AWS 비밀 정보 관리자 콘솔에서:
- 비밀 정보 관리자로 이동>새로운 비밀 정보 저장
- "다른 종류의 비밀"
- 텍스트 탭으로 전환하고 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"
}
- 이름을 지정하세요
myapp/production/secrets - 저장 버튼 클릭
서버 근처의 지역을 선택하세요. Hetzner 박스가 독일에 있으면 eu-central-1 (프랑크푸르트)를 사용하세요. 지연 시간을 낮추고 GDPR을 만족시킵니다.
단계 3: 노트북용 IAM 사용자
노트북이 배포 중 비밀을 읽을 권한이 필요합니다.
- IAM > 사용자 > 사용자 생성
- 이름을
kamal-deploy - 콘솔 접근을 취소 (CLI만 허용)
- 그룹을 생성하며
secrets-managerSecretsManagerReadWrite 정책 - 배치 읽기용 인라인 정책 추가:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:BatchGetSecretValue",
"secretsmanager:ListSecrets"
],
"Resource": "*"
}
]
}
- 사용자를 그룹에 추가
IAM 정책은 몇 분이 걸릴 수 있습니다. 처음에 실패하면 30초를 기다리고 다시 시도하세요.
단계 4: AWS CLI 설정
aws configure
# AWS Access Key ID: paste from IAM user
# AWS Secret Access Key: paste
# 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의 시작을 볼 수 있어야 합니다.
단계 5: .kamal/secrets 파일 형식 지정
여기서 멈췄습니다. --from 플래그는 각 키당 하나의 AWS 비밀번호를 요구합니다. 20개의 분리된 비밀번호는 지루합니다. 확인하세요Kamal 비밀 문서에 대해 더 알아보려면.
대신 Python으로 추출하여 AWS CLI를 사용합니다. 각 줄은 독립적입니다:
# 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은 각 줄을 자신만의 하위 셸에서 평가하므로 줄 간에 공유 변수가 없습니다. 이 작동합니다.
또한 jq를 선호하시면 사용할 수 있습니다:
DEEPGRAM_API_KEY=$(aws secretsmanager get-secret-value --secret-id myapp/production/secrets --query SecretString --output text | jq -r '.DEEPGRAM_API_KEY')
단계 6: 배포
kamal deploy
Kamal은 배포 중 AWS에서 비밀을 가져와 컨테이너에 주입합니다. 평문 파일은 결코 서버에 접촉하지 않습니다.
제작 및 배포
각 환경마다 다른 AWS 비밀번호를 사용합니다. 두 환경 모두 AWS에서 텍스트 없이 가져옵니다.
# .kamal/secrets (used by 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 (used by 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를 실행하면 Kamal이 배포 파일을 읽습니다.
두 비밀이 AWS에 있습니다. 평문의 스테이징 자격 증명도 없습니다. 이는 SOC 2에 중요합니다 왜냐하면 감사인은 모든 환경을 확인하기 때문입니다.
완료되었습니다
더 이상 평문의 비밀이 없습니다. SOC 2와 GDPR 요구 사항을 충족했습니다. Hetzner 청구서는 한 달에 5 유로 미만으로 유지됩니다.
큰 감사를 AWS 문서 팀에게, 그리고 Kamal 유지 관리자들에게 드립니다와 Hetzner를 통해 호스팅 비용을 합리적으로 유지해 주셔서 감사합니다. 이것이 제가 마주했던 고통을 덜어줄 수 있기를 바랍니다. 이제 다시 개발로 돌아가겠습니다.










