<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Danylo Kochetov</title>
    <description>The latest articles on DEV Community by Danylo Kochetov (@inblade).</description>
    <link>https://dev.to/inblade</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4230777%2F993af6ce-51a5-4cb1-94c8-d5eca8c8e5c1.jpg</url>
      <title>DEV Community: Danylo Kochetov</title>
      <link>https://dev.to/inblade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/inblade"/>
    <language>en</language>
    <item>
      <title>From Scheduled Ansible to Continuous Reconciliation on Linux Hosts</title>
      <dc:creator>Danylo Kochetov</dc:creator>
      <pubDate>Fri, 06 Nov 2026 16:05:43 +0000</pubDate>
      <link>https://dev.to/inblade/from-scheduled-ansible-to-continuous-reconciliation-on-linux-hosts-483b</link>
      <guid>https://dev.to/inblade/from-scheduled-ansible-to-continuous-reconciliation-on-linux-hosts-483b</guid>
      <description>&lt;p&gt;A scheduled Ansible playbook is a reasonable way to manage a small Linux fleet.&lt;/p&gt;

&lt;p&gt;It is understandable, widely supported, and easy to introduce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*/15 * * * * ansible-playbook -i inventory production.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 15 minutes, the runner connects to the hosts and applies the playbook again.&lt;/p&gt;

&lt;p&gt;This works. But it answers a narrower question than many teams eventually need.&lt;/p&gt;

&lt;p&gt;It tells us what happened during the last playbook run. It does not necessarily tell us whether the fleet matches Git right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between two runs
&lt;/h2&gt;

&lt;p&gt;Imagine that a playbook runs successfully at 10:00.&lt;/p&gt;

&lt;p&gt;At 10:04, someone edits &lt;code&gt;/etc/nginx/nginx.conf&lt;/code&gt; while investigating an incident.&lt;/p&gt;

&lt;p&gt;Until the next run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Git contains one value;&lt;/li&gt;
&lt;li&gt;the server contains another value;&lt;/li&gt;
&lt;li&gt;no deployment has failed;&lt;/li&gt;
&lt;li&gt;the configuration has drifted;&lt;/li&gt;
&lt;li&gt;the team may not know about it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next scheduled run may repair the file. But the operating model remains periodic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run playbook
    ↓
Wait
    ↓
Something may drift
    ↓
Run playbook again
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful cron job also does not automatically provide a current fleet-level answer to questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which hosts are converged now?&lt;/li&gt;
&lt;li&gt;Which resource is different from Git?&lt;/li&gt;
&lt;li&gt;When did the drift begin?&lt;/li&gt;
&lt;li&gt;Did a host fail to apply the latest revision?&lt;/li&gt;
&lt;li&gt;Is the service healthy after the change?&lt;/li&gt;
&lt;li&gt;Who approved or initiated the reconciliation?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is possible to build these capabilities around Ansible using CI jobs, logs, monitoring, AWX, additional databases, and custom automation.&lt;/p&gt;

&lt;p&gt;The question is whether maintaining that control loop is where the team wants to spend its time.&lt;/p&gt;

&lt;h2&gt;
  
  
  A different operating model
&lt;/h2&gt;

&lt;p&gt;I built hostcd around continuous reconciliation.&lt;/p&gt;

&lt;p&gt;A control plane reads the desired state from Git. A lightweight agent on each host connects outbound to the control plane, receives the accepted desired state, compares it with the host, applies changes, and reports the result.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git repository
       ↓
hostcd control plane
       ↓ outbound agent connection
Linux hosts
       ↓
packages, files, services, users, cron and sysctls
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of waiting for the next scheduled job, each application has an explicit state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Synced or OutOfSync;&lt;/li&gt;
&lt;li&gt;Healthy, Progressing, Degraded, or Missing;&lt;/li&gt;
&lt;li&gt;the Git revision accepted by the host;&lt;/li&gt;
&lt;li&gt;the resource that differs;&lt;/li&gt;
&lt;li&gt;the result of the last reconciliation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference is not YAML versus YAML. It is periodic execution versus a continuously observed control loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  This does not require replacing Ansible
&lt;/h2&gt;

&lt;p&gt;“Replace Ansible” is usually the wrong migration goal.&lt;/p&gt;

&lt;p&gt;Ansible is still useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating and provisioning machines;&lt;/li&gt;
&lt;li&gt;installing the hostcd agent;&lt;/li&gt;
&lt;li&gt;one-off administrative operations;&lt;/li&gt;
&lt;li&gt;network or cloud orchestration;&lt;/li&gt;
&lt;li&gt;unsupported resource types;&lt;/li&gt;
&lt;li&gt;workflows that intentionally run only on demand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A more practical division is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ansible:
provision the machine and perform one-off orchestration

hostcd:
continuously defend the configuration that must remain true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tools can coexist, but they should not manage the same resource at the same time.&lt;/p&gt;

&lt;p&gt;If Ansible writes a file every 15 minutes while hostcd continuously reconciles that file to a different value, the two systems will fight each other.&lt;/p&gt;

&lt;p&gt;Ownership must be explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one small scope
&lt;/h2&gt;

&lt;p&gt;Do not begin by translating the largest production playbook.&lt;/p&gt;

&lt;p&gt;Choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one to three non-critical hosts;&lt;/li&gt;
&lt;li&gt;one package;&lt;/li&gt;
&lt;li&gt;one configuration file;&lt;/li&gt;
&lt;li&gt;optionally one service;&lt;/li&gt;
&lt;li&gt;resources whose expected state is easy to verify.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my pilot, I created an application called &lt;code&gt;ansible-migrated&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It manages two resources on one Ubuntu host:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;code&gt;htop&lt;/code&gt; package;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc/ansible-demo.conf&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application reports one of one hosts Synced and Healthy. Its reconciliation history records the accepted Git revision and the automated sync result.&lt;/p&gt;

&lt;p&gt;This is intentionally unimpressive. A migration test should be easy to understand and difficult to misinterpret.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safe migration sequence
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Record the existing state
&lt;/h3&gt;

&lt;p&gt;Before changing ownership, record:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the playbook and role responsible for the resources;&lt;/li&gt;
&lt;li&gt;the current values on the target hosts;&lt;/li&gt;
&lt;li&gt;the cron or CI schedule;&lt;/li&gt;
&lt;li&gt;the expected rollback procedure;&lt;/li&gt;
&lt;li&gt;any variables or templates that affect the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The existing system remains the reference during the first comparison.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Describe the same state in hostcd
&lt;/h3&gt;

&lt;p&gt;Create the hostcd application and target only the selected hosts.&lt;/p&gt;

&lt;p&gt;Do not enable destructive behaviour until the preview matches expectations. Review the package, file, service, user, and other resource differences before applying them.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Install the agents
&lt;/h3&gt;

&lt;p&gt;The agents connect outbound to the control plane. Regular operation does not require opening inbound agent ports or allowing the control plane to SSH into every managed host.&lt;/p&gt;

&lt;p&gt;Ansible can remain responsible for installing the agent during this stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Compare without creating competing writers
&lt;/h3&gt;

&lt;p&gt;Run hostcd in a non-destructive or review-oriented mode first.&lt;/p&gt;

&lt;p&gt;Confirm that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the expected resources are discovered;&lt;/li&gt;
&lt;li&gt;the calculated differences are correct;&lt;/li&gt;
&lt;li&gt;the selected hosts are correct;&lt;/li&gt;
&lt;li&gt;no unrelated resource is included;&lt;/li&gt;
&lt;li&gt;secrets and generated files are handled appropriately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Remove overlapping Ansible ownership
&lt;/h3&gt;

&lt;p&gt;Once the desired states match, disable the relevant Ansible tasks for the pilot scope.&lt;/p&gt;

&lt;p&gt;Do not disable the entire playbook if only two resources are moving. Remove ownership incrementally.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Reconcile and verify
&lt;/h3&gt;

&lt;p&gt;Apply the accepted Git revision and verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;package state;&lt;/li&gt;
&lt;li&gt;file content and permissions;&lt;/li&gt;
&lt;li&gt;service state;&lt;/li&gt;
&lt;li&gt;application health;&lt;/li&gt;
&lt;li&gt;agent reconnect behaviour after restart.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The UI should report the application as Synced and Healthy, but the verification should also inspect the real host.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Introduce controlled drift
&lt;/h3&gt;

&lt;p&gt;For a non-critical test file, make a manual change on the host.&lt;/p&gt;

&lt;p&gt;Verify that hostcd:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;detects the difference;&lt;/li&gt;
&lt;li&gt;identifies the affected resource;&lt;/li&gt;
&lt;li&gt;reports the application as out of sync;&lt;/li&gt;
&lt;li&gt;restores the Git-managed value when self-heal is enabled;&lt;/li&gt;
&lt;li&gt;records the reconciliation result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not run destructive drift tests against infrastructure anyone depends on.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Observe before expanding
&lt;/h3&gt;

&lt;p&gt;Keep the pilot small for several days.&lt;/p&gt;

&lt;p&gt;Watch for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeated false drift;&lt;/li&gt;
&lt;li&gt;package-manager behaviour;&lt;/li&gt;
&lt;li&gt;service restart loops;&lt;/li&gt;
&lt;li&gt;repository authentication failures;&lt;/li&gt;
&lt;li&gt;host reconnects;&lt;/li&gt;
&lt;li&gt;incorrect targeting;&lt;/li&gt;
&lt;li&gt;resources that still need to remain under Ansible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only then move the next scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about rollback?
&lt;/h2&gt;

&lt;p&gt;With GitOps, the cleanest rollback is usually another Git revision.&lt;/p&gt;

&lt;p&gt;Revert the problematic commit, review the resulting difference, and reconcile the accepted revision.&lt;/p&gt;

&lt;p&gt;This keeps the desired state, approval history, and actual host state aligned.&lt;/p&gt;

&lt;p&gt;A UI-level rollback can be useful, but it needs a clear interaction with automated sync. If the application continuously follows &lt;code&gt;main&lt;/code&gt;, rolling back only the live host without changing the desired revision will cause the controller to move forward again.&lt;/p&gt;

&lt;p&gt;Rollback must change or temporarily override the desired target, not merely copy old files onto a machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational trade-offs
&lt;/h2&gt;

&lt;p&gt;Continuous reconciliation is not automatically better for every resource.&lt;/p&gt;

&lt;p&gt;It introduces an always-running agent and control plane. A bad desired state can be propagated continuously. Automated self-heal can undo an emergency manual change. The agent may require root privileges to manage operating-system resources.&lt;/p&gt;

&lt;p&gt;That means the system needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;narrow targeting;&lt;/li&gt;
&lt;li&gt;dry runs and previews;&lt;/li&gt;
&lt;li&gt;approval gates for sensitive applications;&lt;/li&gt;
&lt;li&gt;audit history;&lt;/li&gt;
&lt;li&gt;per-host authentication;&lt;/li&gt;
&lt;li&gt;signed releases;&lt;/li&gt;
&lt;li&gt;backup and recovery procedures;&lt;/li&gt;
&lt;li&gt;an explicit emergency process for pausing reconciliation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The control loop is valuable only if its authority is bounded and observable.&lt;/p&gt;

&lt;h2&gt;
  
  
  When scheduled Ansible is enough
&lt;/h2&gt;

&lt;p&gt;Keep the existing approach when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the fleet is very small;&lt;/li&gt;
&lt;li&gt;configuration changes rarely;&lt;/li&gt;
&lt;li&gt;manual drift is not a meaningful risk;&lt;/li&gt;
&lt;li&gt;current-state visibility is unnecessary;&lt;/li&gt;
&lt;li&gt;the playbook is primarily provisioning;&lt;/li&gt;
&lt;li&gt;another monitoring system already provides sufficient evidence;&lt;/li&gt;
&lt;li&gt;operating an additional control plane would add more complexity than value.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A cron job is not inherently wrong. It becomes insufficient when the team starts building a second system around it to answer questions the playbook runner was not designed to answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual migration goal
&lt;/h2&gt;

&lt;p&gt;The goal is not to replace one YAML format with another.&lt;/p&gt;

&lt;p&gt;It is to move from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;“The playbook succeeded this morning.”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;“These hosts match this Git revision now.
This resource drifted.
This reconciliation corrected it.
This operator approved it.”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For some teams, scheduled Ansible remains enough.&lt;/p&gt;

&lt;p&gt;For teams that need continuous evidence and automatic drift correction, the safest migration is not a rewrite. It is one package, one file, one host, and one clearly transferred ownership boundary at a time.&lt;/p&gt;

&lt;p&gt;The migration guide is available at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hostcd.dev/guides/ansible-cron-to-hostcd/" rel="noopener noreferrer"&gt;https://hostcd.dev/guides/ansible-cron-to-hostcd/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;hostcd itself is self-hosted and free for up to 10 hosts:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hostcd.dev/" rel="noopener noreferrer"&gt;https://hostcd.dev/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>automation</category>
      <category>linux</category>
      <category>git</category>
    </item>
  </channel>
</rss>
