
September 11, 2026
12 min read
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.
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.
| Tool | Idempotency model | State storage | Best fit |
|---|---|---|---|
| Ansible | Module-level check before change | None (ad hoc facts) | Agentless push, app servers, batch fixes |
| Puppet | Resource catalog reconciliation | Agent local cache + PuppetDB | Large fleets, enforced drift correction |
| Terraform | Plan/apply with state file | Remote backend (S3, etc.) | Cloud infrastructure provisioning |
| Shell scripts | Manual guards you must write | None unless you add it | Small one-off tasks only |
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.
- Declare handlers once at playbook level
- Notify only from tasks that actually mutate state
- Run the playbook twice in CI; fail if handlers would fire on run two
- 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.
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.
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
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.

