Running Ansible From a Pipeline
Scenario: Production is changed from engineers' laptops. There is no record of who ran what, two people once ran conflicting playbooks simultaneously, and the only person who can deploy is on holiday.
New words, in plain English
- Pipeline - automation that runs your playbooks from a controlled, auditable environment.
- Concurrency control - preventing two runs from changing the same estate at once.
- Artefact - a stored output - a run log, a
--diffreport - kept as evidence. - Least privilege - the pipeline holds only the credentials it needs for the environments it deploys.
Running production automation from laptops fails in predictable ways: no audit trail, no concurrency control, inconsistent Ansible and collection versions, and credentials spread across personal machines.
A pipeline fixes all four, and the structure is straightforward:
- On every pull request:
ansible-lint,--syntax-check, and--check --diffagainst a representative subset. Attach the diff to the review as generated evidence. - On merge to main: deploy to staging, then run the verification play.
- To production: a manual approval gate, then the same playbook, with the run log kept as an artefact.
Three operational details matter as much as the structure. Pin everything - the Ansible version and requirements.yml collections - so the pipeline is reproducible. Serialise runs with the pipeline's concurrency controls, so two deployments cannot fight over the same hosts. And hold credentials in the pipeline's secret store, injected at run time, so no long-lived key sits on anyone's laptop.
AWX and Ansible Automation Platform provide this as a product - a web UI, role-based access, job templates, scheduling, surveys and a full audit log. A plain CI pipeline achieves most of it with more assembly and less licensing.
Analogy: The difference between a workshop where anyone can pick up a tool and start cutting, and a production line with a work order, an operator ticket, a log and a supervisor sign-off. The second is slower per change and is the only one you would trust with the safety-critical parts.
A worked example
# .gitlab-ci.yml (or the equivalent) - the shape that matters
stages: [lint, check, staging, production]
variables:
ANSIBLE_FORCE_COLOR: "1"
.setup: &setup
before_script:
- pip install "ansible-core==2.17.7" # PINNED
- ansible-galaxy install -r requirements.yml # PINNED
lint:
stage: lint
<<: *setup
script:
- ansible-lint
- ansible-playbook site.yml --syntax-check
dry-run:
stage: check
<<: *setup
script:
- ansible-playbook site.yml --check --diff --limit canary | tee diff.txt
artifacts:
paths: [diff.txt] # generated change evidence
staging:
stage: staging
<<: *setup
resource_group: staging # never two runs at once
script:
- ansible-playbook site.yml -i inventory/staging
- ansible-playbook verify.yml -i inventory/staging
production:
stage: production
<<: *setup
when: manual # explicit human approval
resource_group: production
script:
- ansible-playbook site.yml -i inventory/production
- ansible-playbook verify.yml -i inventory/production
resource_group (or your platform's equivalent) is the unglamorous line that prevents a genuinely nasty failure mode: two pipelines converging the same hosts with different code, interleaving their changes. It costs one line and removes a class of incident that is very hard to diagnose afterwards.
Keeping the --check --diff output as an artefact gives you something better than a change description: a record of exactly what each deployment intended to change, generated by the tooling, retained alongside the run log. When something goes wrong three weeks later, that record is the fastest route to the answer.
At that point Ansible has stopped being a tool someone runs and become part of the delivery system - which is the whole destination of this track.
Tip: Add resource_group (or equivalent concurrency control) before you add anything else. Two simultaneous production runs is the failure that is hardest to reason about afterwards.
Goal: Put this to work in the ansible-preflight-assertions lab. Open/labs/ansible, pickansible-preflight-assertions, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.