0Pricing
Linux Command Line & Bash Scripting Mastery · 课时

使用 envsubst 和 heredoc 创建配置模板

使用 envsubst 和带引号的 heredoc,从环境变量生成运行时配置。

使用 envsubst 和 heredoc 创建配置模板 是 CoddyKit 上的免费 Linux Command Line & Bash Scripting Mastery 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Linux Command Line & Bash Scripting Mastery 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Linux Command Line & Bash Scripting Mastery 课程共包含 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 导师)并解锁 Linux Command Line & Bash Scripting Mastery 课程的其余内容,请升级到 CoddyKit PRO。 Linux Command Line & Bash Scripting Mastery 课程共包含 4 节课。

「使用 envsubst 和 heredoc 创建配置模板」这节课中我会学到什么?

使用 envsubst 和带引号的 heredoc,从环境变量生成运行时配置。 你通过在浏览器中直接运行的动手代码来练习 Linux Command Line & Bash Scripting Mastery,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Linux Command Line & Bash Scripting Mastery 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Linux Command Line & Bash Scripting Mastery 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「使用 envsubst 和 heredoc 创建配置模板」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Linux Command Line & Bash Scripting Mastery 课中编写并运行代码吗?

能。每节 Linux Command Line & Bash Scripting Mastery 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 编写精简的 Dockerfile 与 Shell 入口点
  2. 使用 envsubst 和 heredoc 创建配置模板
  3. 通过 CLI 和 jq 编写云资源脚本
  4. 健康探针、就绪门禁与等待循环
← 返回 Linux Command Line & Bash Scripting Mastery