Converting Shell Scripts to Ansible With AI
Every team has a pile of bash that should be Ansible. Here's how I use AI to convert shell scripts into idempotent playbooks, and where it gets it wrong.
- #iac
- #ansible
- #ai
- #bash
- #migration
Every team I’ve worked with has a scripts/ directory that’s quietly become load-bearing. There’s a provision.sh that nobody fully understands, a deploy.sh with three different ways to fail silently, and a setup-node.sh that’s been copy-pasted across four repos with subtle differences. Converting that bash to Ansible makes it idempotent, auditable, and runnable across a fleet — but doing it by hand is tedious, which is why it never happens. This is a near-perfect AI task, with one big caveat: a literal translation of bash is usually not good Ansible.
I run AI here as a fast junior engineer that produces a first draft fast, then I do the real work of making it idempotent and reviewing every task. And nothing converted runs against a real host until it’s passed check-mode.
Why bash-to-Ansible isn’t a straight translation
The trap is that bash and Ansible have fundamentally different models. Bash is imperative: “run this command, then this one.” Ansible is declarative: “ensure this state exists.” A naive conversion produces a playbook full of command: and shell: tasks that runs your bash verbatim — which gives you all the downsides of bash plus the overhead of Ansible. The win only comes when you translate intent into modules.
So my prompt is explicit about this:
“Convert this bash script to an Ansible playbook. For each command, use the proper Ansible module rather than
command/shell. Where a command can’t be expressed as a module, keep it ascommandbut add the rightcreates/changed_whenguard. Flag anything you’re unsure about.”
See the difference
Here’s a snippet of typical provisioning bash:
#!/usr/bin/env bash
set -euo pipefail
apt-get update
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
mkdir -p /var/www/app
cp config/app.conf /etc/nginx/conf.d/
A naive conversion would wrap each line in command:. A good conversion, which AI can produce when prompted correctly, uses real modules:
- name: "Provision nginx"
hosts: web
become: true
tasks:
- name: "Install nginx"
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: "Enable and start nginx"
ansible.builtin.service:
name: nginx
enabled: true
state: started
- name: "Ensure app web root exists"
ansible.builtin.file:
path: /var/www/app
state: directory
mode: "0755"
- name: "Deploy nginx app config"
ansible.builtin.copy:
src: config/app.conf
dest: /etc/nginx/conf.d/app.conf
mode: "0644"
notify: reload nginx
Every one of those tasks is idempotent — run it twice and it reports no change. That’s the whole point of the migration, and it’s the part naive translation misses.
Where AI gets it wrong
I’ve done enough of these to know the failure modes. Watch for:
- Lost error handling. Bash’s
set -euo pipefailsemantics don’t map cleanly. AI sometimes drops the intent that a failure should abort. Check thatfailed_whenand task ordering preserve the original guarantees. - Hidden idempotency assumptions. A script that runs
rm -rf /tmp/buildevery time expects a clean slate. Translating tofile: state: absentfollowed bystate: directoryis correct, but AI sometimes “simplifies” it away and changes behavior. - Conditionals flattened wrong. Bash
ifblocks becomewhen:clauses, and AI occasionally inverts the condition. Read everywhen:against the original. commandleft unguarded. When AI does fall back tocommand, it sometimes forgets thecreatesguard, reintroducing non-idempotency. This is the most common miss.
Pro Tip: After conversion, ask AI a second, separate question: “Which of these tasks will report changed on a second run, and why?” Treating idempotency verification as its own pass catches the unguarded command tasks the conversion left behind.
Prove it the same way every time
Converted playbooks earn trust through the same ritual as any other: check-mode, then a real run, then a check-mode re-run that must report zero changes.
ansible-playbook provision.yml --check --diff --limit staging-01
ansible-playbook provision.yml --limit staging-01
ansible-playbook provision.yml --check --diff --limit staging-01 # expect changed=0
That last command is the acid test. If the converted playbook isn’t idempotent, this catches it — exactly the problem the migration was supposed to fix. I also lint:
ansible-lint provision.yml
Keep the secrets in bash out of the prompt
Old shell scripts are notorious for hardcoded secrets — an API key in a variable, a password passed on a command line. Before I paste a script into any AI tool, I redact those and replace them with placeholders. The conversion should move those secrets into ansible-vault anyway, and I do that step by hand. AI sees the structure; it never sees the real key, and it never gets my vault password.
- name: "Write API credentials"
ansible.builtin.template:
src: creds.j2
dest: /etc/app/creds
mode: "0600"
vars:
api_key: "{{ vault_app_api_key }}" # real value lives in vault, not the prompt
Convert incrementally, not all at once
I don’t convert the whole scripts/ directory in one heroic PR. I take one script, convert it, prove it, ship it, and move on. Each conversion is a small reviewable diff a human can actually read. The why AI loves Ansible piece explains why this tool maps so well to AI assistance in the first place, and I keep my conversion prompts in the prompt library so the “use modules, not shell” instruction is always there.
Converting bash to Ansible is one of the highest-leverage things AI does for infrastructure teams, because the tedium was the only thing stopping you. Just remember the draft is a starting point — the idempotency, the error handling, and the secret migration are yours to get right. The rest of the series is under the IaC category, and Warp is handy if you want the conversion happening right in your terminal workflow.
Translate intent, not commands. Let AI draft, you make it idempotent, and prove it with the double check-mode run.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.