Engineering Note
컨테이너 설정 주입의 마지막 한 끗: env에서 config로
컨테이너에 주입된 환경 변수를 애플리케이션이 실제로 읽는 설정 파일로 바꾸는 gomplate 기반 실무 패턴을 정리합니다.
컨테이너에 설정을 넣는 방법은 많습니다. 이미지를 빌드할 때 파일을 넣을 수도 있고, docker run의 command argument로 넘길 수도 있고, 환경 변수로 주입할 수도 있습니다. Kubernetes라면 ConfigMap이나 Secret을 volume 또는 environment variable로 연결할 수 있습니다.
그런데 애플리케이션이 실제로 원하는 것은 종종 이것입니다.
zoo.cfg
nginx.conf
application.yaml
prometheus.yml
즉, 입력은 env, volume, command argument인데 최종 소비자는 설정 파일입니다. 이 마지막 구간을 대충 shell script로 붙이다 보면 entrypoint가 점점 길어지고, 설정 항목이 늘어날 때마다 코드도 같이 고쳐야 합니다.
이 글은 제가 OpenInfra Community Day Korea 2023에서 발표했던 “컨테이너에 초기 설정을 효과적으로 주입하는 방법”을 실무 패턴으로 다시 정리한 글입니다. 핵심 도구는 gomplate입니다.
먼저 가져갈 결론
컨테이너 설정 파일을 생성할 때는 다음 기준을 먼저 잡는 편이 좋습니다.
- 컨테이너 입력 방식과 애플리케이션 설정 파일 형식을 분리합니다.
- 환경 변수 이름 규칙을 정하고, config key로 변환하는 규칙을 명시합니다.
- 모든 key를 템플릿에 직접 나열할지, prefix 기반으로 자동 수집할지 결정합니다.
- 자동 수집을 쓴다면 allowlist 또는 blacklist를 반드시 둡니다.
- secret이 렌더링된 설정 파일과 startup log에 어떻게 남는지 확인합니다.
한 줄로 줄이면 이렇습니다.
환경 변수는 설정 전달 수단이고,
설정 파일은 애플리케이션이 읽는 계약입니다.
컨테이너 설정 주입은 결국 몇 가지로 수렴한다
컨테이너에 설정을 넣는 방법은 다양해 보이지만, 실행 시점 기준으로 보면 보통 몇 가지로 줄어듭니다.
| 방법 | 장점 | 한계 |
|---|---|---|
| 이미지에 미리 포함 | 가장 단순함 | 환경별 변경이 어렵고 이미지가 늘어남 |
| command argument | 실행 시점에 명확함 | 설정이 많아지면 길고 복잡함 |
| environment variable | Docker/Kubernetes에서 다루기 쉬움 | 중첩 구조나 복잡한 파일 형식에 약함 |
| volume mount | 실제 설정 파일을 그대로 제공 | 배포 시스템과 파일 관리가 복잡해질 수 있음 |
| ConfigMap/Secret | Kubernetes와 잘 맞음 | 결국 env 또는 volume으로 주입됨 |
이 중 environment variable은 가장 흔하게 쓰입니다. 문제는 애플리케이션이 env를 직접 읽지 않고 설정 파일을 기대할 때입니다.
단순 매핑은 금방 지친다
예를 들어 Zookeeper 설정을 생각해보겠습니다. 컨테이너 환경 변수는 보통 이런 식입니다.
ZOO_TICK_TIME=2000
ZOO_INIT_LIMIT=5
ZOO_SYNC_LIMIT=2
ZOO_DATA_DIR=/data
ZOO_DATA_LOG_DIR=/datalog
하지만 설정 파일은 이런 key를 기대합니다.
tickTime=2000
initLimit=5
syncLimit=2
dataDir=/data
dataLogDir=/datalog
가장 단순한 방법은 template에 하나씩 적는 것입니다.
tickTime={{ .Env.ZOO_TICK_TIME }}
initLimit={{ .Env.ZOO_INIT_LIMIT }}
syncLimit={{ .Env.ZOO_SYNC_LIMIT }}
dataDir={{ .Env.ZOO_DATA_DIR }}
dataLogDir={{ .Env.ZOO_DATA_LOG_DIR }}
이 방식은 명확합니다. 설정 항목이 적고 오래 바뀌지 않는다면 충분합니다. 하지만 항목이 많아지고, 새 config key가 추가되고, 애플리케이션마다 key naming rule이 다르면 유지보수가 피곤해집니다.
gomplate를 entrypoint에 넣기
gomplate는 Go template 기반의 template renderer입니다. 환경 변수, JSON, YAML, Vault, Consul 같은 datasource를 읽어 템플릿을 렌더링할 수 있습니다. Helm chart를 써본 사람에게는 문법도 꽤 익숙합니다.
컨테이너 entrypoint에서는 보통 이런 흐름이 됩니다.
gomplate -f /templates/zoo.cfg.tmpl -o /conf/zoo.cfg
exec zkServer.sh start-foreground
단순한 직접 매핑에서는 이것만으로 충분합니다. 하지만 이 글의 관심사는 한 단계 더 나아가는 것입니다. 설정 항목을 매번 template에 전부 나열하지 않고, 특정 prefix를 가진 환경 변수를 모아 설정 파일로 바꾸는 패턴입니다.
prefix로 환경 변수 목록을 수집하기
Bash에는 특정 prefix로 시작하는 변수 이름을 가져오는 parameter expansion이 있습니다.
echo ${!ZOO_*}
이렇게 하면 ZOO_로 시작하는 shell variable 이름을 얻을 수 있습니다. 이 목록을 gomplate datasource로 넘기면 template 안에서 반복 처리할 수 있습니다.
echo ${!ZOO_*} | gomplate \
-d config=stdin: \
-f /templates/zoo.cfg.tmpl \
-o /conf/zoo.cfg
template 쪽에서는 stdin datasource를 읽어 key 목록으로 바꿉니다.
{{ $config := datasource "config" }}
{{ $envs := strings.Split " " $config }}
{{ range $key := $envs -}}
{{ $value := env.Getenv $key }}
{{ $key }}={{ $value }}
{{ end }}
물론 이 상태로는 원하는 설정 파일이 나오지 않습니다. ZOO_DATA_DIR=/data 그대로 나오기 때문입니다. 이제 key transform이 필요합니다.
key transform을 명시하기
컨테이너 환경 변수 이름과 설정 파일 key는 보통 naming convention이 다릅니다.
ZOO_DATA_DIR -> dataDir
ZOO_DATA_LOG_DIR -> dataLogDir
ZOO_TICK_TIME -> tickTime
gomplate template에서는 prefix를 제거하고, underscore를 단어 경계로 바꾼 뒤 camelCase로 변환할 수 있습니다.
{{ $config := datasource "config" }}
{{ $envs := strings.Split " " $config }}
{{ range $key := $envs -}}
{{ $value := env.Getenv $key }}
{{ $name := $key | strings.TrimPrefix "ZOO_" }}
{{ $name = $name | strings.ReplaceAll "_" " " | strings.CamelCase }}
{{ $name }}={{ $value }}
{{ end }}
이 패턴의 장점은 새 설정 항목이 추가되어도 naming rule만 맞으면 template을 계속 고치지 않아도 된다는 점입니다.
ZOO_MAX_CLIENT_CNXNS -> maxClientCnxns
ZOO_STANDALONE_ENABLED -> standaloneEnabled
단, 모든 애플리케이션 설정이 camelCase 하나로 끝나지는 않습니다. Zookeeper만 해도 autoPurge.purgeInterval처럼 dot이 들어가는 key가 있습니다. YAML, JSON, TOML은 또 다른 구조를 갖습니다. 그래서 transform rule은 애플리케이션별로 명시해야 합니다.
blacklist보다 중요한 것은 경계다
prefix 기반 자동 수집을 하면 의도하지 않은 환경 변수도 설정 파일에 들어갈 수 있습니다.
예를 들어 이런 값은 런타임 경로일 수는 있지만 설정 파일에 쓰면 안 될 수 있습니다.
ZOO_CONF_DIR=/conf
ZOO_LOG_DIR=/logs
이럴 때 blacklist를 둘 수 있습니다.
{{ $blacklist := coll.Slice "ZOO_CONF_DIR" "ZOO_LOG_DIR" }}
{{ range $key := $envs -}}
{{ if has $blacklist $key }}{{ continue }}{{ end }}
{{ $value := env.Getenv $key }}
{{ $name := $key | strings.TrimPrefix "ZOO_" }}
{{ $name = $name | strings.ReplaceAll "_" " " | strings.CamelCase }}
{{ $name }}={{ $value }}
{{ end }}
하지만 production에서는 blacklist보다 allowlist가 더 안전한 경우도 많습니다. 특히 Secret, token, credential이 같은 prefix를 공유할 가능성이 있다면 자동 수집은 조심해야 합니다.
제 기준은 이렇습니다.
설정 항목이 많고 자주 바뀌며 위험도가 낮으면 prefix + blacklist를 검토한다.
보안 민감 값이 섞이거나 실패 비용이 크면 allowlist를 우선한다.
Kubernetes에서는 어디에 넣을까
Kubernetes에서는 보통 ConfigMap과 Secret이 입력 지점입니다.
envFrom:
- configMapRef:
name: zookeeper-config
- secretRef:
name: zookeeper-secret
또는 ConfigMap을 template 파일로 mount하고, entrypoint에서 gomplate를 실행할 수 있습니다.
/templates/zoo.cfg.tmpl <- ConfigMap volume
/conf/zoo.cfg <- rendered output
중요한 것은 ConfigMap을 쓰느냐 Secret을 쓰느냐가 아닙니다. 애플리케이션 시작 전에 “무엇을 입력으로 받고, 어떤 파일을 생성하며, 실패하면 어떻게 멈출 것인가”를 정하는 것입니다.
entrypoint는 설정 렌더링에 실패하면 애플리케이션을 시작하지 않는 편이 안전합니다.
set -euo pipefail
render_config() {
echo ${!ZOO_*} | gomplate \
-d config=stdin: \
-f /templates/zoo.cfg.tmpl \
-o /conf/zoo.cfg
}
render_config
exec zkServer.sh start-foreground
운영에서 조심할 점
템플릿 렌더링은 편하지만, 운영에서는 몇 가지를 조심해야 합니다.
- 생성된 설정 파일에 secret이 평문으로 남을 수 있습니다.
- startup log에 렌더링 결과를 그대로 출력하면 민감 정보가 노출될 수 있습니다.
- 잘못된 env var 하나가 잘못된 config key를 만들 수 있습니다.
- shell variable naming rule 때문에 hyphen 같은 문자는 피해야 합니다.
- rendered config가 바뀌어도 애플리케이션이 자동 reload하지 않을 수 있습니다.
- template과 entrypoint script도 테스트 대상입니다.
특히 “자동으로 다 모아서 설정 파일에 넣는다”는 패턴은 편한 만큼 경계가 중요합니다. 실무에서는 generated config를 snapshot test처럼 검증하거나, 최소한 container startup test에서 expected config fragment를 확인하는 편이 좋습니다.
컨테이너 설정은 배포 자동화와 운영 진단의 시작점이기도 합니다. Konduo는 여러 인프라 리소스를 플러그인으로 연결하고, 리소스 상태와 메트릭 근거, 알림 대응, 운영 이력을 하나의 흐름에서 다루도록 설계된 통합 관리 운영 플랫폼입니다.
체크리스트
컨테이너에서 env를 config로 바꿀 때는 다음을 확인합니다.
- 애플리케이션이 실제로 읽는 설정 파일 형식은 무엇인가?
- 환경 변수 prefix와 naming convention은 정해져 있는가?
- 직접 매핑이 나은가, prefix 기반 자동 수집이 나은가?
- key transform rule이 문서화되어 있는가?
- blacklist 또는 allowlist가 있는가?
- Secret이 렌더링 결과와 log에 노출되지 않는가?
- 렌더링 실패 시 컨테이너가 즉시 실패하는가?
- generated config를 테스트하거나 검증하는 방법이 있는가?
- 설정 변경 후 reload가 필요한가, restart가 필요한가?
마지막으로 기준을 하나만 남기면 이렇습니다.
컨테이너 설정 주입은 입력을 넣는 문제가 아니라,
애플리케이션이 신뢰할 수 있는 최종 설정 계약을 만드는 문제입니다.
gomplate는 이 마지막 구간을 shell script 덩어리로 숨기지 않고, 템플릿과 변환 규칙으로 드러내는 좋은 선택지입니다.
함께 읽기 좋은 글
이 글의 관점과 이어지는 주제를 더 보려면 아래 글도 함께 읽어볼 수 있습니다.
- Docker Compose는 편한데, .env 비밀값은 너무 쉽게 남는다 - Compose 환경에서 평문 secret 파일을 줄이는 방법을 소개합니다.
- Konduo Community를 Docker Compose로 10분 안에 시작하기 - Konduo Community를 로컬에서 빠르게 실행하고 기본 흐름을 확인합니다.
- JVM 메트릭만으로는 컨테이너를 설명할 수 없다 - JVM 밖의 cgroup, filesystem, fd, I/O 신호를 함께 봐야 하는 이유를 다룹니다.