Ansible Playbooks skill for Codex

Second run reports changed=0, every time. Codex writes Ansible roles that use modules with explicit state instead of shell commands, marks read-only commands as unchanged, validates configs before handlers reload services, and keeps secrets in vaulted vault_ variables with no_log. Rollouts go canary first with serial batches and health checks, and every change is proven by a second staging run that reports zero changes.

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- ansible-playbooks

Installs to ~/.agents/skills/ansible-playbooks/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $ansible-playbooks, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes idempotent Ansible automation - playbooks and roles with FQCN modules, YAML inventories with group_vars and host_vars, handlers, check and diff mode, Ansible Vault for secrets, rolling updates with serial and max_fail_percentage, collection requirements, ansible-lint and Molecule tests. Use when automating server configuration, converting shell scripts to Ansible, structuring roles and inventories, encrypting secrets with ansible-vault, fixing tasks that report changed on every run, or reviewing a playbook before it runs against production hosts.

Use it when

  • Servers or VMs need repeatable configuration
  • Shell scripts that provision hosts should become reviewable automation
  • Secrets used by playbooks need encrypting with Ansible Vault
  • A change must roll across a fleet with a canary and health checks

Use something else when

How to check the result

ansible-lint and --syntax-check pass, and two consecutive runs against staging show the second run with changed=0 on every host.

The mistake it prevents

Wrapping commands in shell without creates or changed_when, so every run reports changes and check mode cannot predict anything.

~/.agents/skills/ansible-playbooks/SKILL.md

--- name: ansible-playbooks description: Writes idempotent Ansible automation - playbooks and roles with FQCN modules, YAML inventories with group_vars and host_vars, handlers, check and… license: MIT ---

A playbook is right when the second run reports changed=0, --check --diff predicts exactly what a real run does, secrets are vaulted and never printed, and a rollout across many hosts stops before a bad change reaches all of them.

Inspect first

  1. Read AGENTS.md, ansible.cfg, the inventory (inventory/, hosts.yml) and requirements.yml for collections and roles.
  2. Check versions: ansible --version (ansible-core 2.18+ expected; 2.19 tightened templating and conditionals, so older playbooks may warn or fail) and ansible-galaxy collection list for collection versions.
  3. Map the inventory: ansible-inventory -i inventory/ --graph and ansible-inventory -i inventory/ --host web1 to see merged variables.
  4. Find existing roles that already manage the thing you are about to touch; two roles writing the same file fight each other on every run.

Running against hosts needs SSH reachability and credentials, and installing collections needs network (Galaxy or a private hub). In a sandbox, write the code and run --syntax-check and ansible-lint locally; leave host runs to the user.

Layout

ansible/
├── ansible.cfg
├── requirements.yml
├── inventory/
│   ├── production/
│   │   ├── hosts.yml
│   │   ├── group_vars/
│   │   │   ├── all.yml
│   │   │   └── web/
│   │   │       ├── vars.yml
│   │   │       └── vault.yml      # ansible-vault encrypted
│   │   └── host_vars/
│   └── staging/
├── playbooks/
│   └── web.yml
└── roles/
    └── nginx/
        ├── defaults/main.yml      # overridable defaults (lowest precedence)
        ├── vars/main.yml          # internal constants (high precedence; rarely needed)
        ├── tasks/main.yml
        ├── handlers/main.yml
        ├── templates/nginx.conf.j2
        ├── meta/main.yml
        ├── meta/argument_specs.yml
        └── molecule/default/

Separate inventories per environment so -i inventory/staging cannot touch production.

# inventory/production/hosts.yml
all:
  children:
    web:
      hosts:
        web1.example.com:
        web2.example.com:
    db:
      hosts:
        db1.example.com:

A good role task file

# roles/nginx/tasks/main.yml
- name: Install nginx
  ansible.builtin.apt:
    name: "nginx={{ nginx_version }}"
    state: present
    update_cache: true
    cache_valid_time: 3600
  become: true

- name: Render site config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: "0644"
    validate: nginx -t -c %s
  become: true
  notify: Reload nginx

- name: Ensure nginx is running and enabled
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true
  become: true
# roles/nginx/handlers/main.yml
- name: Reload nginx
  ansible.builtin.service:
    name: nginx
    state: reloaded
  become: true
# roles/nginx/meta/argument_specs.yml
argument_specs:
  main:
    options:
      nginx_version:
        type: str
        required: true
      nginx_worker_connections:
        type: int
        default: 1024

Argument specs validate role inputs before any task runs.

Idempotency rules

  • Use a module with state: before reaching for command/shell. Modules know whether they changed something; shell commands do not.
  • When command/shell is unavoidable, make it honest:
- name: Initialise the database cluster
  ansible.builtin.command: /usr/lib/postgresql/17/bin/initdb -D /var/lib/postgresql/17/main
  args:
    creates: /var/lib/postgresql/17/main/PG_VERSION
  become: true
  become_user: postgres

- name: Read current schema version
  ansible.builtin.command: /opt/app/bin/migrate version
  register: schema_version
  changed_when: false
  check_mode: false
  • lineinfile for one line in a file you do not own; template for files you do own. Many lineinfile tasks against one file means it should be a template.
  • Quote file modes ("0644"); unquoted 0644 is parsed as an octal int in some contexts and decimal in others.
  • Handlers run once at the end of the play even if notified many times. Use ansible.builtin.meta: flush_handlers when a later task needs the restart to have happened.
  • Use FQCNs (ansible.builtin.copy, community.general.ufw) everywhere.
  • Prefer ansible_facts['distribution'] over injected top-level fact variables like ansible_distribution; recent ansible-core releases deprecate the injected form.
  • ignore_errors: true hides real failures. Use failed_when with an explicit condition.

Secrets with Vault

ansible-vault create inventory/production/group_vars/web/vault.yml --vault-id prod@prompt
ansible-vault encrypt_string --vault-id prod@prompt 's3cr3t' --name 'vault_db_password'
ansible-playbook playbooks/web.yml -i inventory/production --vault-id prod@~/.ansible/prod.vault-pass
  • Convention: vaulted variables are prefixed vault_ in vault.yml, referenced from plain vars.yml (db_password: "{{ vault_db_password }}") so grep still finds every variable.
  • Set no_log: true on tasks that handle secrets; -v output and failure messages otherwise print them.
  • Vault password files live outside the repo with 0600 permissions; never commit them. In CI, read them from the CI secret store.

Safe rollouts

# playbooks/web.yml
- name: Configure web tier
  hosts: web
  serial: [1, "25%"]          # canary one host, then 25 percent batches
  max_fail_percentage: 0
  any_errors_fatal: true
  pre_tasks:
    - name: Drain from load balancer
      ansible.builtin.uri:
        url: "https://lb.internal/api/pools/web/members/{{ inventory_hostname }}/drain"
        method: POST
      delegate_to: localhost
  roles:
    - role: nginx
  post_tasks:
    - name: Health check
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}/healthz"
        status_code: 200
      retries: 10
      delay: 3
      register: health
      until: health.status == 200

Running any playbook against production hosts requires the user’s explicit confirmation after they have seen a --check --diff run against the same inventory and limit.

Workflow

- [ ] 1. Read inventory, cfg, requirements; graph the target hosts
- [ ] 2. Write or change roles; argument specs for new inputs
- [ ] 3. ansible-playbook --syntax-check and ansible-lint
- [ ] 4. Molecule test for the role (if Docker or Podman is available)
- [ ] 5. --check --diff against staging, then a real staging run
- [ ] 6. Second staging run: changed=0
- [ ] 7. With confirmation: production with --limit canary first, then the rest

Verification

ansible-galaxy collection install -r requirements.yml          # network
ansible-playbook playbooks/web.yml -i inventory/staging --syntax-check
ansible-lint playbooks/ roles/
molecule test -s default                                       # in roles/nginx
ansible-playbook playbooks/web.yml -i inventory/staging --check --diff
ansible-playbook playbooks/web.yml -i inventory/staging        # run 1
ansible-playbook playbooks/web.yml -i inventory/staging        # run 2: expect changed=0

--check cannot predict tasks that depend on earlier tasks’ real results (a package that is not installed yet); those show as skipped or failed in check mode. Note them rather than calling the check clean.

Output

**Scope:** role nginx (new), playbook web.yml; inventory staging + production (web group, 6 hosts)
**Lint/tests:** ansible-lint 0 violations, molecule default scenario passes (Debian 12, Ubuntu 24.04)
**Staging:** check/diff reviewed; run 1 changed=7, run 2 changed=0 on all hosts
**Secrets:** vault_tls_key in group_vars/web/vault.yml (vault-id prod), no_log on 2 tasks
**Production:** not run; proposed `--limit web1.example.com` canary, then serial 25%

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Ansible Playbooks"
  short_description: "Second run reports changed=0, every time"
  default_prompt: "$ansible-playbooks Convert this server setup bash script into an Ansible role"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem