Pulumi gives you a beautiful, code-first view of your cloud infrastructure. You define a stack, run pulumi up, and the resources are created exactly as intended. But the moment that state leaves your machine and lands in a live environment, a thousand small changes begin to work against it. Manual SSH sessions, patch agents, autoscaling events, and even well-meaning colleagues can quietly alter what is actually running. Using Ansible as a drift detector for Pulumi-managed servers is the most practical way to combine Pulumi’s declarative state with Ansible’s imperative runtime checks — giving you a safety net that catches drift before it becomes a production incident.
In this article, we’ll look at why Pulumi alone can’t see inside your operating system, how Ansible fits that gap, and what a modern drift-detection workflow looks like in practice.
Why Pulumi’s Declarative State Misses Runtime Drift
It’s tempting to believe that because Pulumi manages a server’s lifecycle, it also manages the server’s contents. In practice, that’s rarely true. Pulumi tracks resources it created: virtual machines, security groups, load balancers, DNS records. Its refresh and preview commands compare that recorded resource state against the cloud provider’s current API state. That catches some drift — say, someone deleted a security group rule in the console, or an Ops team member resized an instance manually.
But Pulumi doesn’t run inside the instance. It cannot tell you that a configuration file was overwritten at 2 a.m., that a systemd unit was masked, that an unauthorized package was installed, or that a service is listening on the wrong port. These are runtime facts, not resource-level facts. They exist on the filesystem, in process tables, and in network sockets. A Pulumi deployment can be perfectly green while the server underneath it is severely degraded. That is exactly the kind of drift that requires an imperative observer.
Ansible’s Imperative Checks Fill the Runtime Void
Ansible is often discussed as an alternative to Pulumi, but the two tools are better understood as complementary layers. Pulumi answers the question “What should exist?” while Ansible answers “What is actually happening?” Ansible is agentless by default, connects over SSH or WinRM, and executes ordered tasks. While many teams use Ansible to configure servers, the same machinery can be turned into a read-only drift detector.
There are several reasons Ansible is well suited for this role:
- Read-only check mode: Ansible’s
--checkflag and modules likeassertlet you inspect state without changing it. - Rich fact collection: Modules like
service_facts,package_facts, andstatgive you a detailed snapshot of runtime conditions. - No persistent agent: You can run a drift-detection playbook on demand from a CI pipeline without installing anything on the target hosts.
- Explicit ordering: Imperative checks are deliberately ordered, making it easy to build a chain of validation steps that mirrors your deployment sequence.
The key mental shift is this: instead of using Ansible to apply configuration, you use it to test configuration. You define a set of expected facts and let Ansible tell you when reality disagrees.
A Drift-Detection Playbook: Checking Without Changing
A well-designed drift-detection playbook never modifies anything. It gathers facts, performs assertions, and fails when the live system doesn’t match your defined baseline. The following snippet is a minimal pattern you can extend to almost any service.
---
- name: Runtime drift checks for Pulumi-managed servers
hosts: all
gather_facts: true
tasks:
- name: Capture current service states
ansible.builtin.service_facts:
- name: Assert that nginx is running and enabled
ansible.builtin.assert:
that:
- ansible_facts.services['nginx.service'].state == 'running'
- ansible_facts.services['nginx.service'].status == 'enabled'
fail_msg: "Drift detected: nginx is not running or not enabled."
success_msg: "nginx state is correct."
- name: Compare checksum of nginx configuration
ansible.builtin.stat:
path: /etc/nginx/nginx.conf
register: nginx_conf
- name: Verify the configuration file has not been modified
ansible.builtin.assert:
that:
- nginx_conf.stat.checksum == "a1b2c3d4e5f67890abcdef1234567890abcd1234"
fail_msg: "Drift detected: nginx.conf checksum mismatch."
success_msg: "nginx.conf matches the known-good baseline."
This playbook doesn’t install, remove, or restart anything. It simply compares the runtime state against the expectations encoded in your repository. When run against a fleet of Pulumi-managed instances, it becomes a repeatable drift audit that can feed into dashboards, chat notifications, or incident workflows.
Where to Run the Drift Detector: CI Gates and Scheduled Scans
There are two complementary ways to run a drift-detection playbook. The first is as a CI gate. After a pulumi up completes, run the Ansible playbook against the freshly provisioned hosts. If drift appears immediately after deployment, you know your Pulumi program is missing an essential step — perhaps an inline script, a user-data block, or a configuration package that should have been part of the image. Finding this at deployment time is vastly cheaper than finding it in a later incident review.
The second pattern is a scheduled scan. Use a cron job, a GitHub Actions schedule, or an automation controller to run the playbook nightly or after peak traffic hours. This catches drift introduced by manual maintenance, untracked changes, or autoscaling events that launched a stale machine image. Running both patterns creates a feedback loop: the CI gate verifies that your deployment logic is complete, while the scheduled scan verifies that your runtime environment behaves after the fact.
Separating Accidental Drift from Acceptable Mutation
Not every change is harmful drift. Servers create logs, cache files, and temporary directories. A web application may legitimately write to a content directory. A database will modify its own data files. A drift-detection system that flags every file change will drown you in false positives. The key is to classify drift into three buckets:
- Acceptable mutation: Runtime-generated content that changes continuously, such as logs, PID files, and application caches. These should be explicitly allowlisted in your playbook.
- Reportable change: A modification that you didn’t expect but that might have a legitimate cause, such as a manual hotfix applied by another engineer. These should be recorded, timestamped, and compared against your change management process.
- Policy violation: A change that puts your security or compliance posture at risk, such as an unauthorized SSH key, a disabled firewall rule, or a downgraded package. These should immediately raise an alert.
Once you define these categories, your Ansible playbook can use separate tasks for each. The result is a drift report that says not only “something changed” but also “this change matters and here is why.”
What to Do When Drift Is Detected
Finding drift is only half of the solution. When a violation is reported, resist the urge to blindly run pulumi up to force the environment back in line. Sometimes the runtime drift represents a genuine operational need that was never captured in code. If an engineer manually adjusted a service to handle an emergency load spike, re-running the Pulumi stack would revert that fix and potentially worsen the incident.
A better workflow is triage first. Look at the drift report, determine whether the change should be canonicalized into your Pulumi stack or Ansible configuration, and then reapply your desired state in a controlled way. If the drift is purely accidental — a misconfigured service or a corrupted file — Ansible can remediate it directly. If the drift is intentional but was never added to your codebase, make that change permanent in source control first, then let automation converge the environment. This keeps the declarative state honest and prevents long-lived divergence between documentation and reality.
Make Drift Visible Before It Becomes an Outage
Pulumi and Ansible are not competing visions of infrastructure management; they are two halves of the same operational story. Pulumi’s declarative model defines what your infrastructure looks like when it is correct. Ansible’s imperative checks observe what your infrastructure is actually doing in real time. Using Ansible as a drift detector for Pulumi-managed servers gives you an early warning system that surfaces small problems while they are still small. The investment is modest — a playbook, a few scheduled runs, and a clear triage path — but the payoff is a production environment that behaves the way your code describes it, even under pressure.
