envsubst와 heredoc으로 구성 템플릿 만들기
envsubst와 따옴표로 묶은 heredoc을 사용하여 환경 변수에서 실행 시 구성 정보를 생성합니다.
envsubst와 heredoc으로 구성 템플릿 만들기은(는) CoddyKit의 무료 DevOps Bootcamp 강의입니다. 이것은 4개 중 2번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DevOps Bootcamp 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DevOps Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
런타임 config 템플릿 생성이 중요한 이유
DevOps 및 컨테이너 작업에서는 nginx.conf, prometheus.yml 및 docker-compose.yml과 같은 config 파일을 스테이징, 프로덕션, DR 등 환경에 따라 변경해야 하는 경우가 많습니다. 값을 하드코딩하면 환경 간 차이가 누적되고 보안 비밀이 노출될 수 있습니다.
해결책은 런타임 config 템플릿 생성입니다. 자리 표시자가 포함된 템플릿을 제공한 다음 시작할 때 환경 변수에서 실제 값을 주입합니다. 이렇게 하면 이미지를 변경 불가능하게 유지하고 config를 감사할 수 있습니다.
- 이미지에 보안 비밀이 포함되지 않음
- 동일한 아티팩트를 여러 환경에 승격할 수 있음
- 프로세스가 시작하기 직전에 config가 생성됨
Bash에서는 두 가지 상호 보완적인 도구인 envsubst와 인용된 heredoc을 사용하여 이 작업을 간단히 처리할 수 있습니다.
envsubst: 한 줄로 만드는 config 생성기
envsubst는 stdin을 읽고, $VARIABLE 및 ${VARIABLE} 자리 표시자를 현재 환경의 값으로 치환한 다음 stdout에 출력하는 작은 GNU 유틸리티입니다.
이 도구는 gettext 패키지에 포함되어 있으며 거의 모든 Linux 배포판과 Docker 베이스 이미지에서 사용할 수 있습니다.
- NGINX, YAML, TOML, JSON, INI 등 모든 텍스트 형식에서 작동합니다.
- 셸 구문을 평가하지 않고 변수 참조만 치환합니다.
- 템플릿 안의 명령을 실행하지 않으므로 안전합니다.
#!/usr/bin/env bash
# Install check (usually already present)
which envsubst || apt-get install -y gettext-base
# Minimal demo
export APP_PORT=8080
export APP_HOST=api.example.com
echo 'server { listen ${APP_PORT}; server_name ${APP_HOST}; }' | envsubst
# Output: server { listen 8080; server_name api.example.com; }선택적 변수 치환
기본적으로 envsubst는 찾은 모든 $VAR를 치환합니다. 이로 인해 $uri나 $host와 같은 NGINX 변수가 망가질 수 있습니다. 이러한 변수는 환경 변수가 아니라 실제 NGINX 지시어입니다.
치환할 이름만 선택하도록 변수 목록을 첫 번째 인수로 명시적으로 전달하십시오.
envsubst '$VAR1 $VAR2'인수는 작은따옴표로 묶은 문자열입니다. 따라서 셸이 이를 확장하지 않으며, 치환할 변수 이름을 공백이나 줄 바꿈으로 구분해 포함합니다.
#!/usr/bin/env bash
export APP_PORT=8080
export APP_HOST=api.example.com
# NGINX template contains both our vars AND nginx vars ($uri, $host)
TEMPLATE='server {
listen ${APP_PORT};
server_name ${APP_HOST};
location / {
proxy_set_header Host $host;
proxy_pass http://backend$uri;
}
}'
# Only substitute APP_PORT and APP_HOST — leave $host and $uri untouched
echo "$TEMPLATE" | envsubst '${APP_PORT} ${APP_HOST}'
디스크에 저장하는 템플릿 파일
실제 config에서는 Dockerfile과 함께 템플릿을 파일로 저장하십시오(예: nginx.conf.template). 컨테이너가 시작할 때 데몬을 실행하기 전에 envsubst를 실행하여 최종 config 파일을 생성합니다.
이는 공식 NGINX Docker 이미지에서 사용하는 표준 패턴입니다.
#!/usr/bin/env bash
# File: nginx.conf.template
# (In practice this lives on disk; we write it here for demo purposes)
cat > /tmp/nginx.conf.template << 'TMPL'
server {
listen ${NGINX_PORT};
server_name ${SERVER_NAME};
root /var/www/${APP_ENV};
location / {
proxy_pass http://app:${APP_PORT};
}
}
TMPL
export NGINX_PORT=80
export SERVER_NAME=myapp.example.com
export APP_ENV=production
export APP_PORT=3000
# Generate final config
envsubst '${NGINX_PORT} ${SERVER_NAME} ${APP_ENV} ${APP_PORT}' \
< /tmp/nginx.conf.template \
> /tmp/nginx.conf
cat /tmp/nginx.conf인용된 히어독: 임시 파일 없이 인라인 템플릿
인용된 히어독(구분자 양쪽에 작은따옴표를 사용한 << 'EOF')은 블록 내부에서 셸이 변수를 확장하거나 명령어 치환을 실행하지 못하게 합니다. 콘텐츠는 리터럴 문자열로 처리됩니다.
따라서 히어독은 템플릿을 인라인으로 작성하고 그 결과를 envsubst로 바로 파이프하는 데 가장 적합합니다. 중간 파일이 필요하지 않습니다.
<< EOF(인용하지 않음) — 셸이$VAR을 즉시 확장합니다<< 'EOF'(인용됨) — 콘텐츠가 리터럴로 유지되고 확장은envsubst로 지연됩니다
#!/usr/bin/env bash
export DB_HOST=postgres.internal
export DB_PORT=5432
export DB_NAME=myapp_prod
# Quoted heredoc: shell does NOT expand $DB_HOST etc. yet
envsubst << 'EOF'
[database]
host = ${DB_HOST}
port = ${DB_PORT}
dbname = ${DB_NAME}
EOF
# Output uses actual env var values — expansion done by envsubst, not the shell히어독과 출력 리디렉션 결합
인용된 히어독을 envsubst로 파이프하고 결과를 한 표현식에서 파일로 리디렉션하십시오. 이 방식은 엔트리포인트 스크립트에서 구성 파일을 생성하는 가장 깔끔한 관용구입니다.
대상 형식(Prometheus, NGINX 등)에 보호해야 할 자체 $variable 구문이 있는 경우 선택적 치환('${VAR1} ${VAR2}')을 사용하십시오.
#!/usr/bin/env bash
# entrypoint.sh — Docker container entrypoint
set -euo pipefail
export PROM_PORT=${PROM_PORT:-9090}
export SCRAPE_INTERVAL=${SCRAPE_INTERVAL:-15s}
export TARGET_HOST=${TARGET_HOST:-localhost:8080}
envsubst '${PROM_PORT} ${SCRAPE_INTERVAL} ${TARGET_HOST}' << 'EOF' > /etc/prometheus/prometheus.yml
global:
scrape_interval: ${SCRAPE_INTERVAL}
evaluation_interval: ${SCRAPE_INTERVAL}
scrape_configs:
- job_name: 'app'
static_configs:
- targets: ['${TARGET_HOST}']
EOF
echo "[entrypoint] Prometheus config written on port ${PROM_PORT}"
exec prometheus --config.file=/etc/prometheus/prometheus.yml --web.listen-address=":${PROM_PORT}"기본값 및 치환 전 검증
모든 필수 변수가 설정되어 있다고 절대 가정하지 마십시오. Bash 매개변수 확장을 사용해 기본값을 제공하거나 명시적으로 실패하게 하십시오:
${VAR:-default}—VAR가 설정되지 않았거나 비어 있으면default를 사용합니다${VAR:?error message}—VAR가 설정되지 않았거나 비어 있으면 오류와 함께 중단합니다
envsubst를 호출하기 전에 이 값을 설정하여 템플릿이 항상 구체적인 값을 받도록 하거나 스크립트가 유용한 메시지와 함께 일찍 중단되도록 하십시오.
#!/usr/bin/env bash
set -euo pipefail
# Required — abort if missing
: "${DATABASE_URL:?DATABASE_URL must be set}"
: "${SECRET_KEY:?SECRET_KEY must be set}"
# Optional with defaults
export APP_PORT=${APP_PORT:-8000}
export LOG_LEVEL=${LOG_LEVEL:-info}
export WORKERS=${WORKERS:-4}
envsubst '${DATABASE_URL} ${SECRET_KEY} ${APP_PORT} ${LOG_LEVEL} ${WORKERS}' \
< /app/config/app.conf.template \
> /app/config/app.conf
echo "[init] Config generated — port=${APP_PORT} workers=${WORKERS} log=${LOG_LEVEL}"여러 히어독으로 다중 섹션 구성 파일 생성
논리적 섹션으로 구성된 복잡한 구성의 경우, 각 섹션을 독립적으로 생성한 후 연결하거나 전체 파일을 아우르는 단일 히어독을 사용할 수 있습니다. 두 방식 모두 작동하므로 가독성에 따라 선택하십시오.
섹션을 조건부로 포함하는 경우(예: 인증서 경로가 설정된 경우에만 TLS 블록 포함)에는 if 블록을 사용하는 여러 히어독 방식이 더 깔끔합니다.
#!/usr/bin/env bash
set -euo pipefail
export APP_HOST=${APP_HOST:-localhost}
export APP_PORT=${APP_PORT:-8080}
export TLS_CERT=${TLS_CERT:-}
export TLS_KEY=${TLS_KEY:-}
CONFIG_FILE=/tmp/app.conf
# Base section
envsubst '${APP_HOST} ${APP_PORT}' << 'BASE' > "$CONFIG_FILE"
[server]
host = ${APP_HOST}
port = ${APP_PORT}
BASE
# Conditional TLS section — only appended when cert is provided
if [[ -n "$TLS_CERT" && -n "$TLS_KEY" ]]; then
envsubst '${TLS_CERT} ${TLS_KEY}' << 'TLS' >> "$CONFIG_FILE"
[tls]
cert_file = ${TLS_CERT}
key_file = ${TLS_KEY}
TLS
echo "[init] TLS enabled"
else
echo "[init] TLS disabled (no cert/key provided)"
fi
cat "$CONFIG_FILE"Docker 엔트리포인트 패턴
권장되는 Docker 엔트리포인트 패턴은 셸 스크립트(docker-entrypoint.sh)를 사용해 시작할 때 구성을 생성한 다음 exec로 주 프로세스에 제어를 넘기는 방식입니다. exec를 사용하면 셸 프로세스가 데몬으로 교체되므로 신호(SIGTERM, SIGINT)가 데몬에 직접 전달됩니다. 이는 정상적인 종료에 매우 중요합니다.
템플릿 파일은 빌드 시 이미지에 추가되고, values는 실행 시 docker run -e 또는 Kubernetes의 env: / envFrom:에서 주입됩니다.
#!/usr/bin/env bash
# docker-entrypoint.sh
set -euo pipefail
# Validate required env vars
for var in DATABASE_URL REDIS_URL SECRET_KEY; do
: "${!var:?$var is required}"
done
export APP_PORT=${APP_PORT:-8000}
export WORKERS=${WORKERS:-$(nproc)}
echo "[entrypoint] Generating configuration..."
envsubst '${DATABASE_URL} ${REDIS_URL} ${SECRET_KEY} ${APP_PORT} ${WORKERS}' \
< /app/config/settings.toml.template \
> /app/config/settings.toml
echo "[entrypoint] Starting server on port ${APP_PORT} with ${WORKERS} workers"
exec gunicorn app:application \
--bind "0.0.0.0:${APP_PORT}" \
--workers "${WORKERS}"Kubernetes ConfigMap + envsubst 패턴
Kubernetes에서는 Pod 사양의 env: 또는 envFrom:을 통해 환경 변수를 주입합니다. 컨테이너 엔트리포인트는 프로세스가 시작되기 전에 envsubst를 호출해 구성을 실제 파일로 만듭니다. 환경마다 ConfigMap을 따로 만들 필요가 없습니다.
이렇게 하면 환경별 값은 Kubernetes 시크릿 및 ConfigMaps에 보관하고(민감하지 않은 데이터의 경우), 구성 템플릿은 이미지에 둡니다. 하나의 이미지로 여러 환경을 지원합니다.
- 빌드:
COPY nginx.conf.template /etc/nginx/templates/ - 실행 시: 엔트리포인트가
envsubst를 실행하고/etc/nginx/nginx.conf를 작성합니다 - K8s 주입: Secret/ConfigMap에서
APP_PORT,BACKEND_HOST
envsubst 디버깅: 누락되거나 해결되지 않은 변수 찾기
생성된 구성에 값 대신 리터럴 ${VAR}가 포함되어 있다면, 해당 변수가 내보내지지 않았거나 치환 목록에 포함되지 않은 것입니다. 다음 기법으로 디버깅하십시오:
printenv | sort— 내보낸 모든 변수 나열- 템플릿 플레이스홀더를 내보낸 변수와
grep으로 비교합니다 envsubst를 실행하고 출력에서 남아 있는${패턴을 검색합니다- 호출 스크립트에서
set -u를 사용하여 Bash 코드의 설정되지 않은 변수 참조가 즉시 실행을 중단하게 합니다
#!/usr/bin/env bash
set -euo pipefail
TEMPLATE=/tmp/app.conf.template
OUTPUT=/tmp/app.conf
# Write a demo template
cat > "$TEMPLATE" << 'EOF'
host=${DB_HOST}
port=${DB_PORT}
name=${DB_NAME}
EOF
export DB_HOST=db.internal
export DB_PORT=5432
# DB_NAME intentionally left unset
envsubst < "$TEMPLATE" > "$OUTPUT"
# Detect unresolved placeholders
if grep -qE '\$\{[A-Z_]+\}' "$OUTPUT"; then
echo "ERROR: unresolved placeholders found:"
grep -oE '\$\{[A-Z_]+\}' "$OUTPUT" | sort -u
exit 1
fi
echo "Config OK:"
cat "$OUTPUT"지식 확인: envsubst 선택적 치환
애플리케이션 변수 ${APP_PORT}와 NGINX 고유 변수 $uri를 모두 포함하는 NGINX 구성 템플릿을 생각해 보십시오. 다음 명령어를 실행합니다:
envsubst < nginx.conf.template > nginx.conf
결과는 무엇입니까?
학습 내용 요약: envsubst와 히어독으로 구성 파일 템플릿 만들기
이제 Bash에서 런타임 구성 생성을 위한 프로덕션 수준 도구 모음을 갖추었습니다:
- envsubst는 현재 환경을 사용해 모든 텍스트 파일의
${VAR}플레이스홀더를 대체합니다. 별도의 스크립팅이나 특수한 이스케이프가 필요하지 않습니다 - 선택적 치환(
envsubst '${VAR1} ${VAR2}')은 NGINX, Prometheus 및 유사한 도구의 고유 변수가 실수로 대체되지 않도록 보호합니다 - 인용된 히어독(
<< 'EOF')은 셸 확장을 지연시켜 템플릿 콘텐츠가 손상되지 않은 상태로envsubst에 전달되게 합니다. 임시 파일이 필요하지 않습니다 - 치환 전에 검증하십시오:
${VAR:?message}를 사용해 필수 변수가 없으면 중단하고, 선택적 변수에는${VAR:-default}를 사용합니다 - Docker 엔트리포인트 패턴: 컨테이너 시작 시 구성을 생성한 다음
exec로 데몬을 실행하여 신호가 올바르게 처리되게 합니다 - 해결되지 않은 플레이스홀더 디버깅: 프로세스가 시작되기 전에 출력에 남아 있는
${패턴을 검색합니다
이러한 패턴을 사용하면 컨테이너 이미지를 변경 불가능하게 유지하고, 소스 제어에서 시크릿을 제외하며, 모든 환경에서 구성을 일관되게 유지할 수 있습니다.
자주 묻는 질문
“envsubst와 heredoc으로 구성 템플릿 만들기” 강의는 무료인가요?
네 — “envsubst와 heredoc으로 구성 템플릿 만들기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DevOps Bootcamp 강의 전체를 잠금 해제할 수 있습니다. DevOps Bootcamp 강의에는 총 4개의 강의가 포함되어 있습니다.
“envsubst와 heredoc으로 구성 템플릿 만들기”에서 뭘 배우나요?
envsubst와 따옴표로 묶은 heredoc을 사용하여 환경 변수에서 실행 시 구성 정보를 생성합니다. 브라우저에서 직접 실행하는 실습 코드로 DevOps Bootcamp을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
DevOps Bootcamp을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 DevOps Bootcamp은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 2번째 강의입니다.
“envsubst와 heredoc으로 구성 템플릿 만들기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 DevOps Bootcamp 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 DevOps Bootcamp 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 간결한 Dockerfile 및 셸 엔트리포인트 작성
- envsubst와 heredoc으로 구성 템플릿 만들기
- CLI와 jq를 활용한 클라우드 리소스 스크립팅
- 상태 프로브, 준비 게이트 및 대기 루프