Connecting Terraform and Ansible with Bash Glue Code
AI generated
$_
#!/
Bash · Terraform · Ansible · Infrastructure as Code
Connecting Terraform and Ansible with Bash Glue Code
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.

18 min read Terraform state · dynamic inventory · jq · CI pipeline Bash 5.x · Terraform 1.x · Ansible 2.15+

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.

11. FAQ: Connecting Terraform and Ansible with Bash Glue Code

1What does Bash glue code mean here?
Scripts that read state, generate inventory and run both tools in the right order instead of manual intermediate steps.
2Isn't a Terraform provisioner enough?
No, provisioners are a last resort due to missing idempotence guarantees. Ansible offers more robust module coverage.
3How do I generate the inventory?
Convert terraform output -json with jq into a YAML inventory, validate it with ansible-inventory --list.
4Avoiding connection refused?
Have a wait function poll port 22 with nc -z before ansible-playbook starts.
5Syncing variables?
Central YAML file as source, generate tfvars.json and Ansible vars from it, prevents drift.
6Handling secrets?
Vault lookup instead of duplicates, set sensitive = true on Terraform outputs.
7Detecting drift automatically?
terraform plan -detailed-exitcode as a cron job, exit code 2 signals pending changes.
8Plan reviews in CI?
Parse terraform show -json, post a summary as a PR comment instead of burying raw output in the log.
9Most common mistake?
Missing set -euo pipefail with tee in a pipe, which reports false success.
10Rollback without native undo?
Back up state before every apply, restore the last good state from a backend with versioning.