New to Kestra?
Use blueprints to kickstart your first workflows.
Audit infrastructure configuration drift in Ansible check mode and conditionally trigger automated playbook remediation with Slack alerts.
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.
daily_drift_check (io.kestra.plugin.core.trigger.Schedule) runs once a day. Shipped disabled so you can validate a manual run first.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.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.notify_slack posts one summary message every run: whether drift was detected, the apply_remediation input value, and whether the remediation task actually executed.apply_remediation) standing between "detected" and "changed", instead of trusting ansible-playbook's own idempotency to be safe enough to run unattended.drift_detected signal via the bundled kestra Ansible module, not a guess from exit codes.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.
AnsibleCLI task defaults to the Docker task runner with the cytopia/ansible:latest-tools image, which bundles ansible-playbook).inventory_content and playbook_content with your fleet's SSH-reachable hosts and baseline tasks.SLACK_WEBHOOK_URL: Slack incoming webhook used by notify_slack.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.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.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.SLACK_WEBHOOK_URL secret.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.inventory_content and playbook_content with your real target hosts and baseline tasks.apply_remediation: true against a host you are comfortable testing on, and confirm the remediation task and Slack message both reflect what happened.daily_drift_check once you trust the audit step; keep apply_remediation as a manual, reviewed flag rather than enabling it on the schedule.check_drift/evaluate_drift over multiple inventory groups to audit several fleets from one flow.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.outputs.check_drift.playbooks into a stored compliance report instead of only a Slack line.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.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.