Schedule icon
AnsibleCLI icon
If icon
Log icon
SlackIncomingWebhook icon

Ansible playbook configuration drift audit and conditional remediation gate

Audit infrastructure configuration drift in Ansible check mode and conditionally trigger automated playbook remediation with Slack alerts.

Categories
CoreInfrastructure

Running ansible-playbook directly is idempotent but all-or-nothing: every run both checks and fixes in the same breath, so there is no safe way to see what would change before anything actually does. This blueprint splits that into two stages. It first runs the playbook in Ansible's own --check (dry-run) mode, which simulates every task without touching the host, and has the playbook report whether it found drift. Only if drift was found, and only if apply_remediation was explicitly set to true, does it run the same playbook for real. Every run, whether it found drift, was gated, or remediated, ends with one Slack message.

How it works

  1. daily_drift_check (io.kestra.plugin.core.trigger.Schedule) runs once a day. Shipped disabled so you can validate a manual run first.
  2. check_drift (io.kestra.plugin.ansible.cli.AnsibleCLI) ships inventory_content and playbook_content as inputFiles (inventory.ini, site.yml) into a cytopia/ansible:latest-tools container and runs ansible-playbook -i inventory.ini site.yml --check --extra-vars "drift_check=true". The default playbook's last task uses the plugin's bundled kestra module to declare drift_detected as an explicit output, based on whether the preceding lineinfile tasks registered changed: true in check mode.
  3. evaluate_drift (io.kestra.plugin.core.flow.If) branches on inputs.apply_remediation and outputs.check_drift.vars.outputs.drift_detected == true. exitCode is deliberately not part of this condition: ansible-playbook --check normally exits 0 whether or not it found drift, so gating on exit code alone would remediate on almost every run regardless of apply_remediation.
    • then: apply_remediation re-runs the identical playbook without --check, actually applying the fix.
    • else: log_audit_only records why nothing ran, no drift, or drift found but not authorized.
  4. notify_slack posts one summary message every run: whether drift was detected, the apply_remediation input value, and whether the remediation task actually executed.

What you get

  • A dry-run-first drift audit that never mutates a host unless you explicitly ask it to.
  • A single boolean gate (apply_remediation) standing between "detected" and "changed", instead of trusting ansible-playbook's own idempotency to be safe enough to run unattended.
  • A playbook-declared drift_detected signal via the bundled kestra Ansible module, not a guess from exit codes.
  • One Slack message per run covering all three outcomes: clean, detected-but-skipped, and remediated.

Who it's for

  • Security and compliance teams who need an audit trail of what would change before anything does.
  • Platform engineers who want remediation to require a conscious flag, not an automatic side effect of a nightly job.
  • Anyone who has been burned by a "monitoring" playbook that silently fixed something nobody reviewed.

Why orchestrate this with Kestra

ansible-playbook has no concept of "check first, then ask before applying" across two separate invocations, that sequencing, the gate in between, and the audit trail of which branch ran are not things a shell script or cron job tracks for you. Kestra's If task turns the gate into a first-class, inspectable branch; the schedule, the explicit apply_remediation input, and the execution history make every audit-only or remediated run reviewable after the fact, and the same flow can be triggered manually with the input flipped on when you are ready to act.

Prerequisites

  • Docker available to the Kestra Worker (the AnsibleCLI task defaults to the Docker task runner with the cytopia/ansible:latest-tools image, which bundles ansible-playbook).
  • A real target inventory and playbook for anything beyond the self-contained localhost default, replace inventory_content and playbook_content with your fleet's SSH-reachable hosts and baseline tasks.
  • A Slack incoming webhook for the summary notification.

Secrets

  • SLACK_WEBHOOK_URL: Slack incoming webhook used by notify_slack.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }}, for example SECRET_SLACK_WEBHOOK_URL=$(echo -n 'https://hooks.slack.com/...' | base64). This keeps the webhook out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.

Inputs

  • apply_remediation (BOOLEAN, default false): must be explicitly true for the remediation branch to run, even if drift is detected.
  • inventory_content (STRING, default localhost ansible_connection=local): INI inventory for the target hosts.
  • playbook_content (STRING, default: a two-task baseline-file playbook): YAML playbook to audit and, if authorized, apply.

Outputs

  • outputs.check_drift.vars.outputs.drift_detected: boolean the playbook declares via the bundled kestra module.
  • outputs.check_drift.exitCode / outputs.apply_remediation.exitCode: raw ansible-playbook exit codes, available for deeper branching if you extend this flow.
  • outputs.check_drift.playbooks / resultsUri: per-host task results from the check-mode run, for building a fuller compliance report.

Quick start

  1. Add the SLACK_WEBHOOK_URL secret.
  2. Run the flow manually with the defaults: it audits a throwaway file inside the Ansible container itself, reports drift_detected: true on a fresh container (there is nothing to be idempotent against yet), and, with the default apply_remediation: false, only logs and alerts without changing anything.
  3. Replace inventory_content and playbook_content with your real target hosts and baseline tasks.
  4. Run once with apply_remediation: true against a host you are comfortable testing on, and confirm the remediation task and Slack message both reflect what happened.
  5. Enable daily_drift_check once you trust the audit step; keep apply_remediation as a manual, reviewed flag rather than enabling it on the schedule.

How to extend

  • Loop check_drift/evaluate_drift over multiple inventory groups to audit several fleets from one flow.
  • Add a Pause approval task between detection and remediation instead of (or in addition to) the static apply_remediation input, so a human confirms each specific drift before it is fixed.
  • Feed outputs.check_drift.playbooks into a stored compliance report instead of only a Slack line.
  • Route to PagerDuty or a ticketing system in addition to Slack when drift is detected but not remediated.

Pitfalls

  • The default inventory proves nothing is persistent. inventory_content defaults to localhost ansible_connection=local, which runs inside the AnsibleCLI task's own ephemeral Docker container. That container does not survive between task runs, so the baseline file this playbook checks never exists on the next run either, drift_detected will read true on every single execution with the defaults. This is intentional for a safe, zero-config first run, not a sign the gate is broken; point inventory_content at a real, persistent host before trusting the audit result.
  • exitCode is not a drift signal. A successful --check run that found drift and one that found nothing both normally exit 0. Do not add outputs.check_drift.exitCode == 0 back into the remediation condition as an "or", it would make the gate fire on almost every run.
  • The playbook must declare drift_detected itself. This gate only works because playbook_content's last task uses the bundled kestra module (kestra: outputs: { drift_detected: ... }). A playbook that omits this task leaves outputs.check_drift.vars.outputs.drift_detected undefined, and the If condition will evaluate it as not equal to true, auditing silently, never remediating.
  • --check cannot simulate every module. Some Ansible modules do not support check mode faithfully (notably ones with real side effects they cannot dry-run); a check-mode "no drift" result is only as good as the modules used in playbook_content.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.