from provisioning to a configured server in one run
Terraform provisions infrastructure, Ansible configures it, but between the two tools most teams leave a manual gap. Bash glue code closes that gap: read state, generate inventory, trigger playbooks, and handle errors in one central place instead of connecting two tools by copy and paste.
Table of Contents
- 1. Why Terraform and Ansible need a bridge
- 2. A wrapper script for terraform init, plan and apply
- 3. Dynamic Ansible inventory from Terraform state
- 4. Syncing variables and secrets between both worlds
- 5. Orchestrating the full run: provisioning to configuration
- 6. Drift detection and idempotence in the Bash workflow
- 7. Driving Terraform and Ansible in a CI pipeline with Bash
- 8. Error handling, rollback and common pitfalls
- 9. Terraform, Ansible and Bash glue code compared
- 10. Summary
- 11. FAQ
1. Why Terraform and Ansible need a bridge
Terraform and Ansible solve different problems, and that is exactly what makes the combination attractive: Terraform declaratively manages cloud resources such as VMs, networks and load balancers, while Ansible takes care of package installation, configuration files and application deployment on machines that already exist. In practice this means someone has to manually enter the IP addresses Terraform just created into an Ansible inventory file before the playbook can even start. This is exactly where the need for Bash glue code arises.
Bash glue code in this context is not a feature tool, it is the adhesive layer between two mature but unconnected systems. A solid piece of Bash glue code reads the Terraform state in a structured way, generates a valid Ansible inventory from it, checks preconditions such as SSH reachability, and only then starts the playbook. Without this glue, Terraform and Ansible remain two separate manual steps with high error potential, especially when several people work in the same repository and maintain the inventory file by hand.
The benefit of consistently maintained Bash glue code shows most clearly with repeated deployments: a new environment, a failover, or a horizontally scaled node pool can then be provisioned and configured with a single command instead of retracing every step manually. The following sections show concrete scripts for wrapping, dynamic inventory, variable sync, and CI integration.
2. A wrapper script for terraform init, plan and apply
The first building block of Bash glue code is a wrapper that runs the three Terraform commands init, plan and apply in a controlled order while adding locking, logging and an explicit confirmation before apply. Without this wrapper, the terraform apply command eventually ends up in a pipeline or a cron job without anyone having reviewed the plan output, which can be disastrous for destructive changes such as rebuilding a database instance.
A central element of this Bash glue code is saving the plan as a file with terraform plan -out=tfplan, so the later apply applies exactly the reviewed plan instead of re planning a second time between plan and apply, which could produce surprises due to state changes in between. The script also uses flock to prevent two colleagues from working on the same state at the same time.
#!/usr/bin/env bash
# tf-wrapper.sh — safe terraform init/plan/apply wrapper as Bash glue code
set -euo pipefail
IFS=$'\n\t'
readonly ENV="${1:?Usage: tf-wrapper.sh <environment>}"
readonly WORKDIR="infra/environments/${ENV}"
readonly LOCK_FILE="/tmp/terraform-${ENV}.lock"
readonly PLAN_FILE="tfplan-${ENV}"
cleanup() {
rm -f "${PLAN_FILE}"
}
trap cleanup EXIT
[[ -d "$WORKDIR" ]] || { echo "[ERROR] Unknown environment: ${ENV}" >&2; exit 1; }
exec 9>"$LOCK_FILE"
flock -n 9 || { echo "[ERROR] Another Terraform run is active for ${ENV}" >&2; exit 1; }
cd "$WORKDIR"
echo "[INFO] terraform init for ${ENV}"
terraform init -input=false -upgrade=false
echo "[INFO] terraform plan for ${ENV}"
terraform plan -input=false -out="../../../${PLAN_FILE}"
read -r -p "Apply this plan for ${ENV}? [yes/NO] " confirm
[[ "$confirm" == "yes" ]] || { echo "[INFO] Aborted by operator"; exit 0; }
echo "[INFO] terraform apply for ${ENV}"
terraform apply -input=false "../../../${PLAN_FILE}"
echo "[INFO] Writing outputs for downstream Ansible step"
terraform output -json > "../../../tf-outputs-${ENV}.json"
This wrapper is deliberately conservative: it prompts before every apply, except in the CI context, where a separate variant runs without an interactive prompt, but instead relies on a strict four eyes principle through pull request review. The last step, writing tf-outputs-ENV.json, is the interface to the next section, this exact file supplies the raw data for the dynamic Ansible inventory.
3. Dynamic Ansible inventory from Terraform state
Ansible needs an inventory that tells it which hosts exist and how they can be reached. Instead of maintaining this file by hand, good Bash glue code generates it automatically from Terraform outputs. Terraform returns a structured JSON output with terraform output -json, which jq can precisely convert into the format Ansible expects, either as a static INI file or as a YAML inventory with groups and host variables.
The decisive advantage of this approach: the inventory file is never stale, because it is regenerated from the current state on every run. If an instance is replaced by Terraform, for example because the AMI ID changed, the new IP address automatically shows up in the next generated inventory without anyone touching a text file. This is the core of what Bash glue code delivers in this workflow, treating Terraform state as the single source of truth.
#!/usr/bin/env bash
# generate-inventory.sh — build Ansible inventory from Terraform JSON output
set -euo pipefail
readonly TF_OUTPUT="${1:?Usage: generate-inventory.sh <tf-outputs.json>}"
readonly INVENTORY_FILE="inventory/generated.yml"
command -v jq >/dev/null || { echo "[ERROR] jq is required" >&2; exit 1; }
[[ -f "$TF_OUTPUT" ]] || { echo "[ERROR] File not found: ${TF_OUTPUT}" >&2; exit 1; }
mkdir -p "$(dirname "$INVENTORY_FILE")"
{
echo "all:"
echo " children:"
echo " web:"
echo " hosts:"
jq -r '.web_instance_ips.value[] | " \(.) :\n ansible_user: deploy\n ansible_ssh_private_key_file: ~/.ssh/deploy_key"' "$TF_OUTPUT"
echo " db:"
echo " hosts:"
jq -r '.db_instance_ips.value[] | " \(.) :\n ansible_user: deploy"' "$TF_OUTPUT"
} > "$INVENTORY_FILE"
echo "[INFO] Inventory written to ${INVENTORY_FILE}"
ansible-inventory -i "$INVENTORY_FILE" --list >/dev/null \
&& echo "[OK] Inventory syntax valid" \
|| { echo "[ERROR] Generated inventory is invalid" >&2; exit 1; }
A detail missing in many implementations of Bash glue code: validating the generated inventory with ansible-inventory --list before the actual playbook runs. This one line prevents a typo in the jq filter logic from only surfacing in the middle of a playbook run, once Ansible already tries to reach hosts it could not parse cleanly.
4. Syncing variables and secrets between both worlds
Terraform variables typically live in .tfvars files, Ansible variables in group_vars and host_vars. Without a deliberate reconciliation, two truths quickly emerge: Terraform knows one database name, Ansible writes it differently in a configuration file. Bash glue code can define a single source here, for example a central YAML file, from which both a terraform.tfvars.json and an Ansible vars file are generated.
Care is required with secrets: Terraform state frequently contains sensitive values in plain text unless they are explicitly marked as sensitive = true. Good Bash glue code consistently filters by naming pattern when transferring values into Ansible variables and instead routes real secrets to a Vault lookup rather than pushing them through the pipeline. The following script shows a simple but effective separation.
#!/usr/bin/env bash
# sync-vars.sh — derive Ansible vars from a single source of truth
set -euo pipefail
readonly SOURCE="config/shared-vars.yml"
readonly TF_VARS_OUT="infra/environments/prod/generated.auto.tfvars.json"
readonly ANSIBLE_VARS_OUT="ansible/group_vars/all/generated.yml"
command -v yq >/dev/null || { echo "[ERROR] yq is required" >&2; exit 1; }
# Non-secret values flow into both tools
yq -o=json '. | with_entries(select(.key != "secrets"))' "$SOURCE" > "$TF_VARS_OUT"
{
echo "# Auto-generated from ${SOURCE} — do not edit by hand"
yq '. | with_entries(select(.key != "secrets"))' "$SOURCE"
echo "db_password: \"{{ lookup('community.hashi_vault.hashi_vault', 'secret=kv/prod/db:password') }}\""
} > "$ANSIBLE_VARS_OUT"
echo "[OK] Generated ${TF_VARS_OUT} and ${ANSIBLE_VARS_OUT}"
echo "[INFO] Secrets were routed to Vault lookups, not copied into either file"
5. Orchestrating the full run: provisioning to configuration
With the wrapper, the inventory generator and the variable sync ready, a top level script ties them into a single run. This orchestration script is the actual core of what teams mean when they talk about Bash glue code: one call that provisions with Terraform, generates the inventory, waits until SSH is reachable, and then starts Ansible.
The order of checks matters: after terraform apply, new instances are visible in the cloud API, but the SSH daemon often still needs a few seconds to come up. A naive script that starts ansible-playbook immediately runs into connection errors. Bash glue code solves this with a wait function that polls via nc or ssh -o ConnectTimeout until the port is open.
#!/usr/bin/env bash
# provision-and-configure.sh — full Terraform-to-Ansible run
set -euo pipefail
readonly ENV="${1:?Usage: provision-and-configure.sh <environment>}"
wait_for_ssh() {
local host="$1" tries=30
until nc -z -w2 "$host" 22 2>/dev/null; do
((tries--)) || { echo "[ERROR] SSH never came up on ${host}" >&2; return 1; }
echo "[INFO] Waiting for SSH on ${host}... (${tries} left)"
sleep 5
done
}
echo "[STEP 1/4] Provisioning infrastructure"
./tf-wrapper.sh "$ENV"
echo "[STEP 2/4] Generating dynamic inventory"
./generate-inventory.sh "tf-outputs-${ENV}.json"
echo "[STEP 3/4] Waiting for SSH on all hosts"
mapfile -t hosts < <(jq -r '.web_instance_ips.value[], .db_instance_ips.value[]' "tf-outputs-${ENV}.json")
for host in "${hosts[@]}"; do
wait_for_ssh "$host"
done
echo "[STEP 4/4] Running Ansible playbook"
ansible-playbook -i inventory/generated.yml site.yml \
--extra-vars "environment=${ENV}"
echo "[DONE] ${ENV} provisioned and configured"
6. Drift detection and idempotence in the Bash workflow
Infrastructure as code only works if the declared state and the actual state match. Drift occurs when someone manually changes a setting in the cloud console that is actually managed by Terraform. Bash glue code can proactively detect drift by regularly running a terraform plan -detailed-exitcode, whose exit code 2 signals that changes are pending, without anything actually being applied.
On the Ansible side, checking idempotence is also worthwhile: a playbook that reports no more changes in check mode (--check --diff) on an already configured server is a strong signal for cleanly written tasks. Bash glue code for drift detection typically runs as a cron job or a scheduled CI job and reports deviations via Slack or email instead of silently ignoring them.
#!/usr/bin/env bash
# drift-check.sh — detect Terraform and Ansible drift, notify without applying
set -euo pipefail
readonly ENV="${1:?Usage: drift-check.sh <environment>}"
readonly SLACK_WEBHOOK="${SLACK_WEBHOOK_URL:?SLACK_WEBHOOK_URL not set}"
notify() {
curl -sf -X POST -H 'Content-Type: application/json' \
-d "{\"text\": \"$1\"}" "$SLACK_WEBHOOK" >/dev/null
}
cd "infra/environments/${ENV}"
terraform init -input=false >/dev/null
set +e
terraform plan -input=false -detailed-exitcode -out=/dev/null
tf_exit=$?
set -e
if [[ $tf_exit -eq 2 ]]; then
notify "[DRIFT] Terraform detected drift in ${ENV} — review required"
elif [[ $tf_exit -eq 1 ]]; then
notify "[ERROR] Terraform plan failed in ${ENV}"
exit 1
else
echo "[OK] No Terraform drift in ${ENV}"
fi
cd -
ansible-playbook -i inventory/generated.yml site.yml --check --diff \
--extra-vars "environment=${ENV}" | tee /tmp/ansible-check.log
if grep -q "changed=[1-9]" /tmp/ansible-check.log; then
notify "[DRIFT] Ansible check-mode reported pending changes in ${ENV}"
fi
7. Driving Terraform and Ansible in a CI pipeline with Bash
In a CI pipeline, the interactive confirmation from the wrapper script disappears, and instead the four eyes principle via merge requests takes the front seat: a Terraform plan is published as a pipeline artifact or comment in the pull request, a human reviewer confirms it, and only then does the apply job run. Bash glue code takes on the role of formatting the plan output readably and posting it as a comment instead of burying the raw Terraform output uncommented in the log.
For GitLab CI or GitHub Actions this means a script that parses terraform show -json, builds a compact summary of added, changed and deleted resources, and posts it as a comment through the respective API. This part of Bash glue code is often the difference between a pipeline reviewers trust and one whose output nobody reads anymore.
#!/usr/bin/env bash
# pipeline-plan-summary.sh — post a readable Terraform plan summary to a PR
set -euo pipefail
readonly PLAN_JSON="${1:?Usage: pipeline-plan-summary.sh <plan.json>}"
readonly PR_API="${CI_MERGE_REQUEST_COMMENTS_URL:?CI_MERGE_REQUEST_COMMENTS_URL not set}"
added=$(jq '[.resource_changes[] | select(.change.actions == ["create"])] | length' "$PLAN_JSON")
changed=$(jq '[.resource_changes[] | select(.change.actions == ["update"])] | length' "$PLAN_JSON")
destroyed=$(jq '[.resource_changes[] | select(.change.actions == ["delete"])] | length' "$PLAN_JSON")
summary="### Terraform Plan Summary
- Resources to add: ${added}
- Resources to change: ${changed}
- Resources to destroy: **${destroyed}**"
if [[ "$destroyed" -gt 0 ]]; then
summary="${summary}
WARNING: this plan destroys existing resources. Review carefully before approving."
fi
curl -sf -X POST -H "PRIVATE-TOKEN: ${CI_JOB_TOKEN}" \
--data-urlencode "body=${summary}" "$PR_API" >/dev/null
echo "[OK] Plan summary posted (create=${added} update=${changed} delete=${destroyed})"
8. Error handling, rollback and common pitfalls
The most common mistake in home grown Bash glue code is missing set -euo pipefail, combined with silently ignored Terraform errors in pipes. When terraform apply | tee apply.log runs without pipefail, the pipeline reports success even if Terraform aborted with an error, because tee always returns exit code 0. The result is a half provisioned state that is considered successful.
Real rollback is fundamentally difficult with Terraform because there is no native undo, but a previous, working state can be restored from the Terraform state history or from a backend with versioning such as S3 with versioning enabled. Bash glue code for rollback backs up a copy of the current state before every apply and automatically restores the last known good state on a failed run, instead of leaving the cluster in an indeterminate intermediate state.
A third pitfall concerns the order of Terraform and Ansible in CI jobs with parallel stages: if the Ansible job starts before the Terraform job has finished writing its output, inventory generation fails. Bash glue code should explicitly chain stages with needs: in GitLab CI or needs: in GitHub Actions instead of relying on implicit ordering.
9. Terraform, Ansible and Bash glue code compared
The following table shows which tool is responsible for which task and where Bash glue code closes the gap between the two.
| Task | Without Bash glue code | With Bash glue code | Benefit |
|---|---|---|---|
| Maintaining inventory | manual INI file | generated automatically from Terraform state | never stale, no typos |
| Connecting after apply | immediate Ansible start | wait function until SSH is ready | no connection refused errors |
| Secrets | duplicated in tfvars and group_vars | Vault lookup, single source | no secret drift between tools |
| Plan review | raw console output in the CI log | structured summary in the PR | reviewers see risk immediately |
| Drift detection | only noticed at the next apply | scheduled cron job with notification | early warning, no surprise apply |
The table makes it clear: Terraform and Ansible remain strong in their respective domains, but only Bash glue code turns two isolated solutions into a coherent workflow that works without manual intermediate steps and stays reproducible in CI pipelines.
Mironsoft
Infrastructure as code, DevOps tooling and automation
Want Terraform and Ansible to actually work together instead of staying two separate tools?
We build Bash glue code that connects Terraform state, dynamic Ansible inventory and CI pipelines into one continuous, traceable provisioning workflow.
IaC audit
Analysis of existing Terraform and Ansible workflows for manual gaps
Wrapper development
Dynamic inventory, locking and error handling as robust Bash glue code
CI integration
Building Terraform plan review, drift detection and rollback into existing pipelines
10. Summary
Bash glue code is the pragmatic answer to the fact that Terraform and Ansible do not understand each other on their own. A wrapper script with locking and explicit plan confirmation makes Terraform runs safe, a generator builds a dynamic, always current Ansible inventory from Terraform state, a central variable source prevents drift between the configurations of both tools, and a wait function for SSH reachability avoids race conditions right after provisioning.
In CI pipelines, Bash glue code additionally takes on the role of summarizing Terraform plans readably and proactively reporting drift instead of leaving it to chance when someone notices a manually changed server. Once these building blocks are set up cleanly, new environments, failover scenarios or scaling events can be run with a single command instead of wiring every step by hand again.
Connecting Terraform and Ansible with Bash Glue Code — The essentials
Dynamic inventory
Convert Terraform output into Ansible inventory with jq, never maintain it by hand, always current.
Wrapper with locking
flock and saved plans prevent parallel state changes and unreviewed applies.
Keep secrets separate
Vault lookup instead of duplicates in tfvars and group_vars, one single source of truth.
Detect drift proactively
terraform plan -detailed-exitcode as a cron job reports deviations before they become a problem.