envsubstとheredocによる設定のテンプレート化
envsubstとクォートしたheredocを使い、環境変数から実行時設定を生成します。
「envsubstとheredocによる設定のテンプレート化」はCoddyKit上の無料DevOps Bootcampレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはDevOps Bootcamp学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 DevOps Bootcampコースには全4レッスンが含まれています。
ランタイムでの設定テンプレート展開が重要な理由
DevOpsやコンテナのワークフローでは、nginx.conf、prometheus.yml、docker-compose.ymlなどの設定ファイルを、ステージング、本番、DRといった環境ごとに変更することがよくあります。値をハードコードすると、環境間の差異やシークレットの漏えいが発生します。
解決策はランタイムでの設定テンプレート展開です。プレースホルダーを含むテンプレートを配布し、起動時に環境変数から実際の値を注入します。これにより、イメージを不変に保ち、設定を監査可能にできます。
- イメージにシークレットを組み込まない
- 同じアーティファクトを複数の環境に昇格できる
- プロセスの起動直前に設定を生成できる
Bashでは、envsubstとクォートしたheredocという2つの補完的なツールによって、これを簡単に実現できます。
envsubst:1行で設定を生成するツール
envsubstは、標準入力を読み取り、$VARIABLEおよび${VARIABLE}形式のプレースホルダーを現在の環境にある値へ置き換え、標準出力へ書き出す小さな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}'
ディスク上のテンプレートファイル
実際の設定では、テンプレートをファイルとして(例: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')を使うと、ブロック内の変数展開やコマンド置換がシェルによって実行されなくなります。内容は文字列リテラルとして扱われます。
これにより、ヒアドキュメントはテンプレートをインラインで記述し、そのまま 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 に通し、1つの式で結果をファイルにリダイレクトします。これは、エントリーポイントスクリプトで設定ファイルを生成するための最もすっきりした定型方法です。
対象形式(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}"複数のヒアドキュメントによる複数セクション設定の生成
論理的なセクションで構成される複雑な設定では、各セクションを個別に生成して連結することも、ファイル全体を1つのヒアドキュメントで記述することもできます。どちらの方法でも機能するため、読みやすさに基づいて選択してください。
セクションを条件付きで含める場合(たとえば、証明書パスが設定されている場合にのみ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)がデーモンに直接届きます。これは正常なシャットダウンに不可欠です。
テンプレートファイルはビルド時にイメージへ追加し、値は実行時に 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の Secrets と ConfigMaps(機密性のないデータ用)で管理し、設定テンプレートはイメージ内に保持できます。1つのイメージで複数の環境に対応できます。
- ビルド:
COPY nginx.conf.template /etc/nginx/templates/ - 実行時:エントリーポイントが
envsubstを実行し、/etc/nginx/nginx.confに書き込みます - Kubernetesが注入:Secretまたは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の選択的置換
アプリケーション変数 ${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でデーモンを起動してシグナルを正しく処理します - 未解決のプレースホルダーをデバッグする:プロセスを開始する前に、出力に残っている
${パターンをgrepで探します
これらのパターンにより、コンテナイメージを不変に保ち、シークレットをソース管理から除外し、あらゆる環境で設定の一貫性を維持できます。
よくある質問
「envsubstとheredocによる設定のテンプレート化」レッスンは無料ですか?
はい。「envsubstとheredocによる設定のテンプレート化」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、DevOps Bootcampコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 DevOps Bootcampコースには全4レッスンが含まれています。
「envsubstとheredocによる設定のテンプレート化」で何を学びますか?
envsubstとクォートしたheredocを使い、環境変数から実行時設定を生成します。 ブラウザで直接実行するハンズオンコードでDevOps Bootcampを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
DevOps Bootcampを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのDevOps Bootcampは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。
「envsubstとheredocによる設定のテンプレート化」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このDevOps Bootcampレッスンでコードを書いて実行できますか?
はい。すべてのDevOps Bootcampレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 軽量なDockerfileとシェルエントリポイントの作成
- envsubstとheredocによる設定のテンプレート化
- CLIとjqによるクラウドリソースのスクリプト操作
- ヘルスプローブ、準備完了ゲート、待機ループ