- Posted on
- • Artificial Intelligence
Automatically Generate Deployment Scripts
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
Automatically Generate Deployment Scripts with Bash (and Zero Tears)
You’ve been there: dev works, staging drifts, prod is “special,” and a Friday hotfix ends up running the wrong script. Manual edits and copy-pasting between environments are slow, error-prone, and hard to audit.
Here’s the fix: treat deployment scripts as generated artifacts. Keep your configuration in one place, use templates, and auto-render the exact script you need for each environment. In this guide you’ll learn a simple, portable Bash pattern to generate deployment scripts automatically—fast, repeatable, and version-controlled.
Why this works (and keeps working)
Consistency over time: Templates ensure every environment gets the same logic, with only data changing.
Safer rollouts: Generated scripts reduce “one-off” edits that cause outages.
Auditability: Git history (template + config) explains what changed and why.
Speed: One command to produce a ready-to-run, environment-specific script.
This approach is pure Bash with two tiny helpers (jq and envsubst) available on every major distro.
What we’ll build
A single JSON file as the source of truth for environment config.
A Bash template with placeholders for values like
HOST,APP_NAME, andIMAGE_TAG.A renderer script that produces
dist/deploy-dev.sh,dist/deploy-staging.sh, anddist/deploy-prod.sh.An optional Makefile to tie it all together.
No Docker/Kubernetes/Ansible required—though you can drop those commands right into the template if you use them.
Prerequisites: install the tooling
We’ll use:
jqto read JSON configenvsubst(from gettext) to replace placeholdersgitandmake(optional but recommended)
Install with your package manager:
Debian/Ubuntu (apt):
sudo apt-get update sudo apt-get install -y jq gettext-base git makeFedora/RHEL/CentOS (dnf):
sudo dnf install -y jq gettext git makeopenSUSE (zypper):
sudo zypper refresh sudo zypper install -y jq gettext-runtime git make
Check they’re available:
jq --version
envsubst --version
git --version
make --version
1) Define your configuration (the source of truth)
Create a deploy.json describing common values and per-environment overrides.
{
"common": {
"APP_NAME": "hello-svc",
"REGISTRY": "ghcr.io/acme",
"PORT": 8080
},
"dev": {
"HOST": "dev.acme.local",
"IMAGE_TAG": "dev-2026-07-06",
"REPLICAS": 1
},
"staging": {
"HOST": "staging.acme.com",
"IMAGE_TAG": "rc-1.4.2",
"REPLICAS": 2
},
"prod": {
"HOST": "app.acme.com",
"IMAGE_TAG": "v1.4.2",
"REPLICAS": 3
}
}
Tip: Keep secrets out of this file. Inject secrets at runtime (CI variables, .env files, secret stores), not in Git.
2) Write a safe, portable Bash template
Put your real deployment logic here, but keep environment-specific values as placeholders. Name it deploy.tmpl.sh.
#!/usr/bin/env bash
set -Eeuo pipefail
# These are replaced during generation
APP_NAME="${APP_NAME}"
REGISTRY="${REGISTRY}"
IMAGE_TAG="${IMAGE_TAG}"
HOST="${HOST}"
REPLICAS="${REPLICAS}"
PORT="${PORT}"
ENV="${ENV}"
log() { printf '[%s] %s\n' "$(date -Iseconds)" "$*"; }
die() { echo "ERROR: $*" >&2; exit 1; }
require() {
command -v "$1" >/dev/null 2>&1 || die "Missing dependency: $1"
}
main() {
# Example: sanity checks
[[ -n "$APP_NAME" && -n "$IMAGE_TAG" && -n "$HOST" ]] || die "Missing required vars"
# Example: print what will happen (replace with your real commands)
log "Deploying ${APP_NAME}:${IMAGE_TAG} to ${HOST} (env=${ENV}) with ${REPLICAS} replicas"
log "Registry: ${REGISTRY}, Port: ${PORT}"
# Replace these echo lines with real steps: ssh, kubectl, systemctl, rsync, etc.
# Example (ssh-based):
# require ssh
# ssh deploy@${HOST} "sudo systemctl stop ${APP_NAME} || true"
# ssh deploy@${HOST} "curl -fsSL ${REGISTRY}/${APP_NAME}:${IMAGE_TAG} -o /opt/${APP_NAME}/release.tgz"
# ssh deploy@${HOST} "tar -xzf /opt/${APP_NAME}/release.tgz -C /opt/${APP_NAME} && sudo systemctl start ${APP_NAME}"
log "Done."
}
trap 'rc=$?; [[ $rc -eq 0 ]] || echo "Deployment failed with exit $rc" >&2' EXIT
main "$@"
Notes:
Use
set -Eeuo pipefailto catch errors early.Keep logic generic; you’ll fill in real deployment steps for your stack.
3) Render environment-specific scripts automatically
Create render.sh to read JSON, export variables, and render the template via envsubst.
#!/usr/bin/env bash
set -Eeuo pipefail
ENVIRONMENT="${1:-dev}"
CONFIG="${CONFIG:-deploy.json}"
TEMPLATE="${TEMPLATE:-deploy.tmpl.sh}"
OUTDIR="${OUTDIR:-dist}"
OUTFILE="${OUTDIR}/deploy-${ENVIRONMENT}.sh"
require() {
command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 1; }
}
require jq
require envsubst
[[ -f "$CONFIG" ]] || { echo "Config not found: $CONFIG" >&2; exit 1; }
[[ -f "$TEMPLATE" ]] || { echo "Template not found: $TEMPLATE" >&2; exit 1; }
# Validate env exists in config
jq -e --arg env "$ENVIRONMENT" '.[$env] // empty' "$CONFIG" >/dev/null || {
echo "Environment '$ENVIRONMENT' not found in $CONFIG" >&2
exit 1
}
mkdir -p "$OUTDIR"
# Export ENV so it’s available to the template
export ENV="$ENVIRONMENT"
# Merge common + env-specific, then export key/value pairs safely via TSV
while IFS=$'\t' read -r key val; do
export "$key=$val"
done < <(jq -r --arg env "$ENVIRONMENT" '
(.common // {}) * (.[ $env ] // {})
| to_entries[]
| [.key, ( .value | tostring )]
| @tsv
' "$CONFIG")
# Render
envsubst < "$TEMPLATE" > "$OUTFILE"
chmod +x "$OUTFILE"
echo "Generated: $OUTFILE"
Try it:
bash render.sh dev
./dist/deploy-dev.sh
Then:
bash render.sh prod
./dist/deploy-prod.sh
4) Automate with Makefile targets
Optional but nice for CI and local workflows. Create Makefile.
SHELL := /usr/bin/env bash
ENVS := dev staging prod
.PHONY: all $(ENVS) clean check
all: $(ENVS)
$(ENVS):
@bash render.sh $@
@echo "OK: dist/deploy-$@.sh"
check:
@command -v jq >/dev/null || { echo "jq missing"; exit 1; }
@command -v envsubst >/dev/null || { echo "envsubst missing"; exit 1; }
@[ -f deploy.json ] && [ -f deploy.tmpl.sh ] || { echo "Missing files"; exit 1; }
@echo "All checks passed"
clean:
@rm -rf dist
Now you can:
make check
make
./dist/deploy-staging.sh
5) Production-grade extras
Validation: Add a small validator step in
render.shto ensure required keys exist:jq -e '.common.APP_NAME and .common.REGISTRY' deploy.json >/dev/null || { echo "Missing required keys in deploy.json" >&2; exit 1; }Dry run: In your template, gate mutating commands behind
if [[ "${DRY_RUN:-0}" == "1" ]]; then echo "..."; else real_command; fi.Versioning: Stamp scripts with Git info:
GIT_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')" export GIT_SHAThen reference
${GIT_SHA}in the template header.Idempotency: Make your template safe to re-run (stop-if-running, create-if-missing).
CI integration: Run
makeon merge to main; publishdist/artifacts as build outputs or attach to releases.
Real-world variations
Kubernetes: Template
kubectlcommands (namespace, image tag, replicas) or even render small YAML fragments withenvsubst.Systemd: Render a
.servicefile with the correct ExecStart and environment, thensystemctl daemon-reload && systemctl restart.SSH fan-out: Generate per-environment scripts that run the same steps against different hosts with
Parallel/GNU parallel(optional).
Wrap-up and next steps
Stop copy-pasting scripts between dev/staging/prod. Put your values in one JSON file, your logic in one template, and generate the exact script you need with a single command. It’s reproducible, fast, and safe.
Your next steps:
1) Copy the snippets above into a new repo (deploy.json, deploy.tmpl.sh, render.sh, Makefile).
2) Install jq and envsubst with your package manager (see commands above).
3) Run make and execute the generated scripts in a test environment.
4) Replace the example echo lines with your real deployment commands.
When you’re ready, wire this into CI and make “manual prod deploys” a thing of the past.