使用 envsubst 和 heredoc 创建配置模板
使用 envsubst 和带引号的 heredoc,从环境变量生成运行时配置。
使用 envsubst 和 heredoc 创建配置模板 是 CoddyKit 上的免费 DevOps Bootcamp 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 DevOps Bootcamp 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 DevOps Bootcamp 课程共包含 4 节课。
为什么运行时配置模板很重要
在 DevOps 和容器工作流中,nginx.conf、prometheus.yml 和 docker-compose.yml 等配置文件通常需要在不同环境之间变更,例如预发布环境、生产环境和灾难恢复环境。硬编码值会造成配置漂移并导致机密泄露。
解决方案是运行时配置模板:随镜像发布带有占位符的模板,然后在启动时从环境变量注入实际值。这样既能保持镜像不可变,也便于审计配置。
- 不会将机密写入镜像
- 同一个构建产物可以在不同环境之间逐步发布
- 在进程启动前立即生成配置
在 Bash 中,两种互补工具可以轻松实现这一点:envsubst 和带引号的 heredoc。
envsubst:用一行命令生成配置
envsubst 是一个小型 GNU 工具,会读取标准输入,将 $VARIABLE 和 ${VARIABLE} 占位符替换为当前环境中的对应值,然后写入标准输出。
它随 gettext 软件包一起提供,几乎所有 Linux 发行版和 Docker 基础镜像都支持它。
- 适用于任何文本格式:NGINX、YAML、TOML、JSON、INI
- 不会计算 Shell 语法,只会替换变量引用
- 安全:不会执行模板中的命令
#!/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。这可能会破坏 NGINX 变量,例如 $uri 或 $host,因为它们是真正的 NGINX 指令,并不是您的环境变量。
将变量的明确列表作为第一个参数传入,使替换范围仅限于指定名称:
envsubst '$VAR1 $VAR2'该参数是一个单引号字符串(因此 Shell 不会展开它),其中包含您希望替换的变量名称,名称之间用空格或换行分隔。
#!/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}'
磁盘上的模板文件
对于实际配置,请将模板作为文件存储(例如 nginx.conf.template),并与 Dockerfile 放在一起。在容器启动时,运行 envsubst 生成最终配置文件,然后再启动守护进程。
这是官方 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',在分隔符两侧使用单引号)可以防止 Shell 在代码块内展开变量或运行命令替换。内容会被视为字面字符串。
这使内嵌文档成为内联编写模板并将其直接通过管道传给 envsubst 的理想方式——无需中间文件。
<< EOF(不带引号)— Shell 会立即展开$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 入口模式是使用 Shell 脚本(docker-entrypoint.sh)在启动时生成配置,然后通过 exec 将控制权交给主进程。使用 exec 会用守护进程替换 Shell 进程,因此信号(SIGTERM、SIGINT)会直接到达守护进程——这对优雅关闭至关重要。
模板文件会在构建时添加到镜像中;运行时的值则来自 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 - Kubernetes 注入:从机密对象或 ConfigMap 注入
APP_PORT、BACKEND_HOST
调试 envsubst:查找缺失或未解析的变量
当生成的配置中出现字面量 ${VAR} 而不是具体值时,说明该变量未导出,或者未包含在替换列表中。您可以使用以下方法进行调试:
printenv | sort— 列出所有已导出的变量- 使用
grep将模板占位符与已导出的变量进行比较 - 运行
envsubst,并使用 grep 在输出中查找剩余的${模式 - 在调用脚本中使用
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 的选择性替换
请考虑一个 NGINX 配置模板,其中同时包含应用程序变量 ${APP_PORT} 和 NGINX 原生变量 $uri。您运行以下命令:
envsubst < nginx.conf.template > nginx.conf
结果是什么?
课程回顾:使用 envsubst 和内嵌文档配置模板
现在,您已经掌握了一套可用于生产环境的 Bash 运行时配置生成工具:
- envsubst 使用当前环境中的值替换任何文本文件里的
${VAR}占位符——无需编写脚本,也无需特殊转义 - 选择性替换(
envsubst '${VAR1} ${VAR2}')可以保护 NGINX、Prometheus 及类似工具中的原生变量,避免它们被意外替换 - 带引号的内嵌文档(
<< 'EOF')会延后 Shell 展开,使模板内容完整地传给envsubst——无需临时文件 - 替换前先验证:使用
${VAR:?message}在缺少必需变量时中止,使用${VAR:-default}为可选变量提供默认值 - Docker 入口模式:在容器启动时生成配置,然后使用
exec启动守护进程,以便正确处理信号 - 在进程启动前,通过在输出中查找剩余的
${模式来调试未解析的占位符
这些模式可以让您的容器镜像保持不可变,让机密信息远离源代码管理,并让配置在每个环境中保持一致。
常见问题解答
「使用 envsubst 和 heredoc 创建配置模板」课时是免费的吗?
是的 — 「使用 envsubst 和 heredoc 创建配置模板」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 DevOps Bootcamp 课程的其余内容,请升级到 CoddyKit PRO。 DevOps Bootcamp 课程共包含 4 节课。
「使用 envsubst 和 heredoc 创建配置模板」这节课中我会学到什么?
使用 envsubst 和带引号的 heredoc,从环境变量生成运行时配置。 你通过在浏览器中直接运行的动手代码来练习 DevOps Bootcamp,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 DevOps Bootcamp 需要有经验吗?
无需任何先前经验。CoddyKit 上的 DevOps Bootcamp 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「使用 envsubst 和 heredoc 创建配置模板」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 DevOps Bootcamp 课中编写并运行代码吗?
能。每节 DevOps Bootcamp 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 编写精简的 Dockerfile 与 Shell 入口点
- 使用 envsubst 和 heredoc 创建配置模板
- 通过 CLI 和 jq 编写云资源脚本
- 健康探针、就绪门禁与等待循环