Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Idempotency in Configuration Management

By Kokil Thapa | Last reviewed: September 2026

Idempotency in configuration management is the property that applying the same configuration twice leaves the system unchanged the second time. A broken deploy script that always restarts services or rewrites files creates outages, drift, and midnight pages. On production Laravel stacks I maintain with Linux system administration workflows, idempotent playbooks are what let you rerun Ansible after a partial failure without guessing what already changed. This guide covers the concept, tool behaviour, copy-paste patterns, and how to test reruns before they hit live servers.

What is idempotency in configuration management?

Idempotency comes from mathematics: f(f(x)) = f(x). In ops terms, the first run brings a server from its current state to the target state. Every later run confirms nothing extra is needed.

Configuration management (CM) tools express desired state—packages installed, files present, services running—and reconcile reality against that declaration. The reconciliation step is what makes CM different from a naive bash script that blindly executes commands.

Consider installing Nginx on Ubuntu 24.04. An imperative script runs apt install nginx every time. That may work, but it wastes time and can trigger unnecessary service reloads. An idempotent approach asks: is Nginx installed at the required version? Only if the answer is no does it install.

Desired State ReconciliationDesired StatePlaybook / ManifestCM EngineAnsible / PuppetCurrent StateLive Server FactsIdempotent OutcomeRun 1: changes applied (changed=3)Run 2: no changes (changed=0, ok=3)
Idempotency in configuration management: the engine compares desired and current state before making changes

The Puppet configuration management basics article covers resource abstraction in depth. Puppet, Ansible, Chef, and Salt all share this reconcile model, though syntax and agent architecture differ.

Idempotency is not the same as immutability. Immutable infrastructure replaces whole servers from images. Configuration management keeps long-lived servers aligned with declared policy. Many teams blend both: Terraform provisions, Ansible configures, and Deployer deploys application code.

Why does idempotency matter for production servers?

Production servers drift. Someone edits /etc/php/8.4/fpm/pool.d/www.conf by hand. A package upgrade changes a default. Without idempotent tooling, you cannot safely re-apply baseline config during incident response.

On sister legal-tech sites I deploy with Deployer 7 and GitLab CI, the application release is separate from OS-level configuration. When PHP-FPM pool settings or fail2ban jails need correction, I rerun Ansible playbooks rather than SSH in manually. Idempotency guarantees the second run only fixes what drifted.

Non-idempotent scripts create real damage:

  • Appending the same cron line on every deploy until duplicates flood the scheduler
  • Recreating database users and resetting passwords unintentionally
  • Restarting services during peak traffic because a script always calls systemctl restart
  • Corrupting config files by duplicating blocks on each run

Idempotent CM also improves auditability. Tools report changed, ok, or failed per resource. A second run that reports zero changes proves the server matches policy. That evidence matters for compliance and post-incident reviews.

For PHP OPcache configuration for production, idempotent file modules ensure ini settings stay correct after PHP minor upgrades. Manual edits get overwritten intentionally—once—then held steady.

How do Ansible and Puppet implement idempotent configuration?

Each major CM tool enforces idempotency differently. Understanding the mechanism helps you debug "changed on every run" bugs.

Ansible modules

Ansible modules are idempotent by design. The apt, copy, template, user, and systemd modules inspect state before acting. Run a playbook twice; the second pass should show changed=0 for converged tasks.

- name: Ensure PHP 8.4 FPM is installed
  ansible.builtin.apt:
    name: php8.4-fpm
    state: present
    update_cache: true

- name: Deploy pool configuration
  ansible.builtin.template:
    src: templates/www.conf.j2
    dest: /etc/php/8.4/fpm/pool.d/www.conf
    owner: root
    group: root
    mode: "0644"
  notify: Reload php8.4-fpm

- name: Ensure PHP-FPM is running and enabled
  ansible.builtin.systemd:
    name: php8.4-fpm
    state: started
    enabled: true

The template module checksums destination files. If content matches, no write occurs and handlers do not fire. That is idempotency protecting you from unnecessary reloads.

Puppet resources

Puppet declares resources with a type and title. The Puppet agent compares each resource to catalog state every 30 minutes (by default). Unchanged resources produce no events.

package { 'nginx':
  ensure => installed,
}

file { '/etc/nginx/sites-available/laravel.conf':
  ensure  => file,
  content => template('site/laravel.conf.erb'),
  notify  => Service['nginx'],
}

service { 'nginx':
  ensure => running,
  enable => true,
}

Read the comparison in Terraform vs Ansible provisioning vs configuration management. Terraform tracks state files and plans diffs. Ansible and Puppet inspect live systems. All three aim for safe reruns, but state storage differs.

ToolIdempotency modelState storageBest fit
AnsibleModule-level check before changeNone (ad hoc facts)Agentless push, app servers, batch fixes
PuppetResource catalog reconciliationAgent local cache + PuppetDBLarge fleets, enforced drift correction
TerraformPlan/apply with state fileRemote backend (S3, etc.)Cloud infrastructure provisioning
Shell scriptsManual guards you must writeNone unless you add itSmall one-off tasks only
First Run vs Second RunRun 1 — Initial ApplyInstall packagesWrite configschanged=8 ok=12Run 2 — ConvergedSkip unchanged tasksNo handler triggerschanged=0 ok=20Failure SignalRun 2 still shows changed > 0Fix non-idempotent task or external drift
A healthy idempotent configuration management pipeline reports zero changes on the second consecutive run

Official Ansible documentation describes modules as idempotent building blocks. The Ansible glossary entry on idempotency is the authoritative reference when you need precise terminology.

How do you write idempotent shell scripts and custom tasks?

Not everything belongs in Ansible. Sometimes you wrap legacy installers or glue steps in shell tasks. Those fragments must be idempotent too, or they poison the whole playbook.

Guard clauses pattern

Test before mutate. This pattern installs Composer only if the binary is missing:

#!/bin/bash
set -euo pipefail

COMPOSER_BIN="/usr/local/bin/composer"

if [[ ! -x "$COMPOSER_BIN" ]]; then
  curl -sS https://getcomposer.org/installer | php
  mv composer.phar "$COMPOSER_BIN"
  chmod 755 "$COMPOSER_BIN"
  echo "installed"
else
  echo "already present"
fi

In Ansible, prefer the creates argument on command or shell modules:

- name: Run Laravel optimize (only if cache dir empty)
  ansible.builtin.command:
    cmd: php artisan config:cache
    chdir: /var/www/laravel/current
  args:
    creates: /var/www/laravel/current/bootstrap/cache/config.php

The creates parameter skips the command when the file exists. Use it sparingly—application deploy steps often belong in Deployer, not CM.

File line management

Never use bare echo >> for config lines. Use lineinfile or blockinfile in Ansible, or Puppet's augeas / file_line resources.

- name: Ensure UFW allows HTTP
  ansible.builtin.ufw:
    rule: allow
    port: "80"
    proto: tcp

For fail2ban jails, see the Ubuntu fail2ban configuration guide. Copy-pasting jail blocks into /etc/fail2ban/jail.local without guards is a classic non-idempotent mistake.

Handlers and notification discipline

Handlers run only when notified by a changed task. That prevents service restarts on no-op runs. A common bug is using register + when: result.changed incorrectly and restarting anyway.

  1. Declare handlers once at playbook level
  2. Notify only from tasks that actually mutate state
  3. Run the playbook twice in CI; fail if handlers would fire on run two
  4. Separate application deploy restarts from OS config playbooks

Application-level idempotency has its own rules. Payment webhooks and API POST endpoints need idempotency keys—a topic covered in the API idempotency keys implementation guide. CM idempotency protects infrastructure; API idempotency protects business data.

What breaks idempotency and how do you detect drift?

Even good tools fail idempotency when misused. These patterns show up repeatedly on client servers.

Using command/shell without guards. Running mysql -e "CREATE USER..." fails or duplicates on rerun. Use dedicated modules or idempotent SQL with IF NOT EXISTS patterns.

Non-deterministic templates. Embedding {{ ansible_date_time.epoch }} in a config file forces a change every run. Keep timestamps out of managed files unless you accept perpetual churn.

Race conditions with parallel tasks. Two tasks managing the same file without coordination cause flip-flop changes. Consolidate into one template task.

External drift. A human edits a managed file. The next run corrects it—that is working idempotency, not a bug. Communicate this to teams who expect manual edits to stick.

Idempotent vs Imperative ScriptsIdempotent CMChecks state firstSafe to rerun anytimeReports changed vs okSelf-heals driftCI can gate on changed=0Imperative ScriptAlways executes stepsRisky on second runNo change reportingDrift accumulates silentlyManual SSH fixes pile upPrefer CM for anything that runs more than once
Idempotent configuration management versus one-shot imperative scripts on long-lived production servers

Drift detection options include Ansible Check Mode (--check), Puppet's --noop, and dedicated tools like osquery or InSpec. For Laravel multi-server setups, session and cache config drift breaks login flows—see Laravel session configuration for multi-server for app-layer alignment that CM cannot fully solve alone.

Puppet's declarative model is documented in the Puppet resources language guide. If your catalog flaps between states, the resource dependency graph is usually wrong.

How do you test idempotency in CI/CD pipelines?

Idempotency is testable. You should automate the two-run check before playbooks touch production. I run this pattern on shared EC2 hosts that serve multiple Notary Kathmandu sister properties.

Two-run gate in GitLab CI

stages:
  - lint
  - test-idempotency
  - deploy

ansible-idempotency:
  stage: test-idempotency
  image: cytopia/ansible-lint
  script:
    - ansible-playbook -i inventory/staging site.yml
    - ansible-playbook -i inventory/staging site.yml | tee second_run.log
    - grep -E "changed=[1-9]" second_run.log && exit 1 || exit 0

If the second run reports any changes, the pipeline fails. Investigate before promoting to production. Common culprits are shell tasks, floating package versions, and timestamped content.

Combine this with CI/CD secrets management best practices. Idempotent playbooks still need vault-encrypted variables—reruns must not leak credentials into logs.

Molecule for role testing

Molecule spins up Docker or Vagrant instances, applies your Ansible role twice, and asserts idempotency. For teams maintaining reusable roles across projects, it pays for itself quickly.

# molecule/default/converge.yml
- name: Converge
  hosts: all
  roles:
    - role: php_fpm_laravel

Run molecule converge twice; the second converge must show zero tasks changed. Integrate into the same GitLab pipeline stage above.

CI Idempotency Test PipelineGit PushPlaybook changeansible-lintSyntax checkRun 1 ApplyStaging targetRun 2 Verifychanged must = 0Pass → ProductionDeployer releasePHP-FPM reloadFail → BlockFix non-idempotent taskNo prod SSH panic
Testing idempotency in configuration management in CI before allowing Deployer releases to production

Staging inventory should mirror production: same Ubuntu version, PHP 8.4 or 8.5 builds, and matching service names. Idempotency on Ubuntu 22.04 does not guarantee behaviour on 24.04 if package names differ.

Application deploy tools like Deployer are idempotent at the release level—running dep deploy twice should leave the current symlink on the latest successful release. OS configuration and app deployment stay separate concerns. Mixing them in one bash script erodes both.

For secrets rotation, pair idempotent file tasks with vault tooling from secrets management with HashiCorp Vault. Rotated secrets should update managed files once, then hold steady across reruns.

Need to validate JSON vars files before they enter a playbook? Use the on-site JSON formatter and validator to catch syntax errors early. Broken JSON in group_vars fails late and wastes pipeline minutes.

When hiring help, look for teams that treat CM as infrastructure code—not one-off SSH sessions. Our support and maintenance services include playbook-driven server baselines for Laravel and WordPress stacks. Domain registration and hosting decisions affect whether you can run agentless Ansible or need Puppet agents behind NAT.

HashiCorp publishes complementary guidance on infrastructure lifecycle in the Terraform resource behaviour documentation. Terraform plans are idempotent at the infrastructure graph level; combine them with Ansible for in-guest configuration.

Key Takeaways

  • Idempotency in configuration management means reruns converge to desired state without duplicate side effects—second run should report zero changes.
  • Use native Ansible modules and Puppet resources instead of raw shell; add guard clauses when shell is unavoidable.
  • Separate OS configuration playbooks from application deploy steps (Deployer, GitLab CI) to keep blast radius small.
  • Automate the two-run CI gate: fail the pipeline if the second Ansible apply shows any changed tasks.
  • Watch for timestamps, unguarded commands, and manual server edits that cause flapping or silent drift.
  • Test on staging inventory that matches production PHP, Ubuntu, and service naming before promoting playbooks live.

People Also Ask

Is Terraform idempotent?

Yes, within its state model. Terraform plan compares desired configuration to tracked state and live APIs. A second terraform apply with no config changes produces an empty plan. State corruption or resources created outside Terraform break this guarantee until you import or refresh state.

Can bash scripts be idempotent?

They can, but you must write every guard yourself—check file existence, use CREATE IF NOT EXISTS, test package presence with dpkg -s. Maintenance cost rises fast. Prefer CM modules for anything that runs weekly or more often.

What is the difference between idempotency and convergence?

Convergence is the process of reaching desired state over one or more runs. Idempotency is the property that once converged, further runs cause no additional changes. Puppet agents converge continuously; idempotency ensures those cycles do not destabilize services.

Why does my Ansible playbook change something every run?

Usually a shell/command task without guards, a template that includes dynamic timestamps, or a module bug with floating versions. Run with -vvv, identify the changing task, and replace it with a proper module or add explicit state checks.

Build servers you can safely rerun

Idempotency in configuration management turns scary reruns into routine hygiene. Declare what the server should look like, test the second apply in CI, and keep application deploys separate from OS baselines. That is how you sleep through scheduled Ansible jobs instead of dreading them.

If your fleet still depends on one-off shell history, start with a single playbook—PHP-FPM, Nginx, UFW—and enforce the two-run gate. Need hands-on help baselining Laravel or legal-tech stacks? Contact us or review production deployment work on the portfolio. For broader context on infrastructure as code, browse the technical blog and read about testing and optimization for performance hardening after CM converges.

Frequently Asked Questions

Idempotency means applying the same configuration twice leaves the system unchanged the second time. CM tools compare desired state to current state and only act when they differ.

Yes, within its state model. Terraform plan compares desired config to tracked state and live APIs. A second apply with no changes produces an empty plan unless state is corrupted or resources were created outside Terraform.

Yes, but you write every guard yourself—check file existence, test packages with dpkg -s, use IF NOT EXISTS. Maintenance cost rises fast; prefer Ansible or Puppet modules for recurring tasks.

Production servers drift when someone edits PHP-FPM pool files by hand or package upgrades change defaults. Without idempotent tooling, you cannot safely re-apply baseline config during incident response. Non-idempotent scripts append duplicate cron lines, reset database passwords, restart services during peak traffic, or corrupt config files by duplicating blocks. Idempotent CM also improves auditability: a second run reporting zero changes proves the server matches policy, which matters for compliance and post-incident reviews on long-lived Laravel stacks.

Ansible modules such as apt, copy, template, user, and systemd inspect live state before acting. Run a playbook twice and the second pass should show changed=0 for converged tasks. The template module checksums destination files; if content matches, no write occurs and handlers do not fire. That prevents unnecessary PHP-FPM reloads when pool configuration is already correct. Official Ansible documentation describes modules as idempotent building blocks, and the Ansible glossary entry on idempotency is the authoritative terminology reference when debugging behaviour.

Puppet declares resources with a type and title—package, file, service—and the agent compares each resource to catalog state every 30 minutes by default. Unchanged resources produce no events. A file resource with templated content notifies the service resource only when content actually changes. Puppet stores state in an agent local cache plus PuppetDB, making it well suited to large fleets where enforced drift correction runs continuously rather than on ad hoc playbook pushes like Ansible.

Idempotency keeps long-lived servers aligned with declared policy through reconciliation—rerunning playbooks fixes drift without unnecessary side effects. Immutability replaces whole servers from images instead of mutating them in place. Many production teams blend both: Terraform provisions cloud resources, Ansible configures in-guest settings, and Deployer deploys application code. They solve different problems. CM idempotency protects OS-level baselines on servers that persist for months; immutability protects against configuration rot by rebuilding from known-good images when that model fits your deployment cadence.

Use guard clauses that test before mutating: check if a binary exists before downloading Composer, or use the creates argument on command and shell modules to skip when an output file already exists. Never use bare echo append for config lines—use lineinfile, blockinfile, or dedicated modules like ufw instead. Wrap legacy installers in shell tasks only when no native module exists. Application deploy steps such as php artisan config:cache often belong in Deployer rather than CM playbooks, because mixing OS configuration and app deployment in one script erodes idempotency for both layers.

Common culprits include command or shell tasks without guards—CREATE USER fails or duplicates on rerun—non-deterministic templates embedding timestamps like ansible_date_time.epoch that force changes every run, and parallel tasks managing the same file without coordination causing flip-flop state. Floating package versions without pinned ensures values also cause perpetual changed reports. External drift from manual edits is not a bug; the next run correcting a human-edited managed file is working idempotency. If your Puppet catalog flaps between states, the resource dependency graph is usually wrong and needs consolidation into single template tasks.

Convergence is the process of reaching desired state over one or more runs—the first apply may change many resources while the system moves from drifted reality toward declared policy. Idempotency is the property that once converged, further runs produce no duplicate side effects and should report zero changes. A healthy pipeline achieves convergence on run one and idempotency on run two. They are related but not identical: a tool can converge slowly across multiple runs while still being idempotent once the target state is reached and held steady.

Handlers run only when notified by a task that actually changed state, preventing service restarts on no-op playbook runs. Declare handlers once at playbook level and notify only from tasks that mutate files or packages. A common bug is using register plus when: result.changed incorrectly and restarting services anyway during peak traffic. Run the playbook twice in CI and fail if handlers would fire on the second run. Keep application deploy restarts in Deployer separate from OS config playbooks so PHP-FPM reloads happen only when pool configuration genuinely changes, not on every pipeline execution.

Drift detection options include Ansible Check Mode with --check, Puppet's --noop flag, and dedicated tools like osquery or InSpec. An idempotent second run that reports changed tasks on a supposedly stable server reveals drift from manual edits or failed prior applies. For Laravel multi-server setups, session and cache config drift can break login flows at the application layer—CM alone cannot fully solve that alignment problem. Communicate to teams that manual edits to managed files will be overwritten intentionally on the next playbook run, which is expected reconciliation behaviour rather than a tooling failure.

Add a test-idempotency stage that runs ansible-playbook twice against staging inventory, then greps the second run log for changed=1 or higher and fails the pipeline if any task reports changes. Combine this with lint stages using ansible-lint. Common second-run failures come from shell tasks, floating package versions, and timestamped template content—investigate before promoting to production. Staging inventory should mirror production: same Ubuntu version, matching PHP 8.4 or 8.5 builds, and identical service names, because idempotency on Ubuntu 22.04 does not guarantee behaviour on 24.04 if package names differ.

Molecule spins up Docker or Vagrant instances, applies your Ansible role, and lets you assert that a second converge shows zero tasks changed. Configure a converge.yml that applies your role—such as a php_fpm_laravel role—and integrate molecule converge twice into the same GitLab pipeline idempotency stage. For teams maintaining reusable roles across multiple client projects, it pays for itself quickly by catching non-idempotent shell fragments before they reach shared EC2 hosts serving several production properties. It complements the two-run playbook gate rather than replacing staging tests against production-like inventory.

Yes. On production Laravel stacks, application release through Deployer 7 and GitLab CI stays separate from OS-level configuration managed by Ansible. When PHP-FPM pool settings or fail2ban jails need correction, rerun Ansible playbooks rather than SSH in manually. Deployer is idempotent at the release level—running dep deploy twice should leave the current symlink on the latest successful release. Mixing OS config and app deployment in one bash script erodes idempotency for both and enlarges blast radius. Payment webhooks and API endpoints need their own application-layer idempotency keys; CM idempotency protects infrastructure while API idempotency protects business data.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: