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, and IMAGE_TAG.

  • A renderer script that produces dist/deploy-dev.sh, dist/deploy-staging.sh, and dist/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:

  • jq to read JSON config

  • envsubst (from gettext) to replace placeholders

  • git and make (optional but recommended)

Install with your package manager:

  • Debian/Ubuntu (apt):

    sudo apt-get update
    sudo apt-get install -y jq gettext-base git make
    
  • Fedora/RHEL/CentOS (dnf):

    sudo dnf install -y jq gettext git make
    
  • openSUSE (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 pipefail to 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.sh to 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_SHA
    

    Then reference ${GIT_SHA} in the template header.

  • Idempotency: Make your template safe to re-run (stop-if-running, create-if-missing).

  • CI integration: Run make on merge to main; publish dist/ artifacts as build outputs or attach to releases.

Real-world variations

  • Kubernetes: Template kubectl commands (namespace, image tag, replicas) or even render small YAML fragments with envsubst.

  • Systemd: Render a .service file with the correct ExecStart and environment, then systemctl 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.