Idempotency Patterns¶
Mooncake provides several features to help you write idempotent playbooks that can be safely run multiple times without unintended side effects.
Table of Contents¶
Using creates¶
The creates field skips a step if the specified file path already exists. This is useful for one-time installation or setup tasks.
One-time installation¶
- name: Download installer
shell: wget https://example.com/installer.sh -O /tmp/installer.sh
creates: /tmp/installer.sh
- name: Run installer
shell: bash /tmp/installer.sh
creates: /opt/myapp/bin/myapp
On the first run, both steps execute. On subsequent runs, both steps are skipped because the files exist.
Compilation steps¶
The compilation only runs if the binary doesn't exist yet.
Template variables¶
The creates path is rendered through the template engine, so you can use variables:
- name: Set build directory
vars:
build_dir: /opt/myproject
- name: Compile project
shell: make build
creates: "{{ build_dir }}/myapp"
Using unless¶
The unless field skips a step if the given command succeeds (returns exit code 0). This provides more flexibility than creates for conditional execution.
Database initialization¶
- name: Initialize database
shell: psql -f schema.sql mydb
unless: "psql -c '\\dt' mydb | grep users"
The initialization only runs if the users table doesn't exist.
Service configuration¶
- name: Configure service
shell: systemctl enable myservice
unless: "systemctl is-enabled myservice"
The service is only enabled if it's not already enabled.
Version checks¶
- name: Install package
shell: apt-get install -y mypackage
unless: "dpkg -s mypackage | grep -q 'Version: 2.0'"
The package is only installed if version 2.0 is not currently installed.
Template variables¶
Like creates, the unless command is rendered through the template engine:
- name: Set database name
vars:
db_name: production
- name: Create database
shell: createdb {{ db_name }}
unless: "psql -l | grep {{ db_name }}"
Note: The unless command is executed silently (no output logged) to avoid cluttering the logs.
Using changed_when¶
The changed_when field allows you to override whether a shell command is marked as "changed". By default, all shell commands are marked as changed.
Commands with predictable output¶
- name: Install package
shell: apt-get install -y package
register: install_result
changed_when: "'is already the newest version' not in result.stdout"
The step is only marked as changed if the package was actually installed or upgraded.
Always-safe commands¶
Setting changed_when: false indicates this command is idempotent and doesn't make changes.
Conditional based on exit code¶
- name: Check and update config
shell: diff config.new /etc/config && cp config.new /etc/config
register: config_result
changed_when: result.rc == 0
failed_when: false
Only mark as changed if the files were different (diff returns 0) and the copy succeeded.
Combining Strategies¶
You can combine creates, unless, when, and changed_when for sophisticated idempotency control.
Smart package installation¶
- name: Install package
shell: apt-get install -y mypackage
creates: /usr/bin/mypackage
register: pkg_install
changed_when: "result.rc == 0 and 'already installed' not in result.stdout"
failed_when: "result.rc != 0 and 'Unable to locate package' not in result.stderr"
This step:
- Skips if
/usr/bin/mypackagealready exists - Only marks as changed if the package was actually installed
- Only fails if there's a real error (not just "package not found")
Conditional with multiple checks¶
- name: Install development tools
shell: apt-get install -y build-essential
when: ansible_os_family == "Debian"
unless: "dpkg -l build-essential | grep '^ii'"
creates: /usr/bin/gcc
This step only runs if:
- The OS family is Debian (via
when) - The package is not already installed (via
unless) - The compiler doesn't exist (via
creates)
Evaluation order: when → creates → unless → execute
Database setup with safeguards¶
- name: Create database user
shell: |
psql -c "CREATE USER myapp WITH PASSWORD '{{ db_password }}';"
unless: "psql -c '\\du' | grep myapp"
register: user_created
changed_when: result.rc == 0
- name: Grant privileges
shell: |
psql -c "GRANT ALL PRIVILEGES ON DATABASE mydb TO myapp;"
when: user_created.changed
unless: "psql -c '\\l' mydb | grep myapp | grep -q PRIVILEGES"
The second step only runs if the user was just created or if privileges aren't already granted.
Result Timing¶
All step results now include timing information that can be accessed in registered results:
- name: Run expensive operation
shell: make build
register: build_result
- name: Show build duration
shell: echo "Build took {{ build_result.duration_ms }}ms"
Available timing fields¶
When you register a result, the following timing fields are available:
result.duration_ms: Duration in milliseconds (integer)result.status: String status ("ok", "changed", "failed", "skipped")
Example: Performance monitoring¶
- name: Compile project
shell: make -j4 build
register: compile
- name: Run tests
shell: make test
register: tests
- name: Report performance
shell: |
echo "Compilation: {{ compile.duration_ms }}ms"
echo "Tests: {{ tests.duration_ms }}ms"
echo "Total: {{ compile.duration_ms + tests.duration_ms }}ms"
Example: Conditional based on performance¶
- name: Run optimization
shell: optimize-database
register: optimize_result
- name: Alert if slow
shell: |
echo "Warning: Optimization took {{ optimize_result.duration_ms }}ms" | \
mail -s "Slow optimization" admin@example.com
when: optimize_result.duration_ms > 60000
This sends an alert if optimization takes more than 60 seconds (60000ms).
Best Practices¶
1. Prefer creates for file-based idempotency¶
Use creates when you're creating files or installing software that produces files:
# Good
- name: Download file
shell: wget https://example.com/file.tar.gz
creates: file.tar.gz
# Less efficient
- name: Download file
shell: wget https://example.com/file.tar.gz
unless: "test -f file.tar.gz"
creates is more efficient because it uses a simple filesystem check.
2. Use unless for state checks¶
Use unless when idempotency depends on system state rather than file existence:
3. Combine with register for dependent steps¶
- name: Install package
shell: apt-get install -y nginx
creates: /usr/sbin/nginx
register: nginx_installed
- name: Start nginx
shell: systemctl start nginx
when: nginx_installed.changed
The service is only started if nginx was just installed.
4. Document non-obvious idempotency¶
- name: Apply database migrations (idempotent via migration tracking table)
shell: ./migrate.sh
changed_when: result.stdout | contains('Applied migrations')
Add comments when idempotency isn't immediately obvious from the command.
5. Test your idempotency¶
Always run your playbook at least twice to verify it's truly idempotent:
# First run - should make changes
mooncake run -c playbook.yml
# Second run - should skip most steps
mooncake run -c playbook.yml
Common Patterns¶
Package management¶
- name: Install package
shell: apt-get install -y package-name
creates: /usr/bin/package-name
# Or with unless
- name: Install package
shell: apt-get install -y package-name
unless: "dpkg -l package-name | grep '^ii'"
File downloads¶
- name: Download archive
shell: wget https://example.com/archive.tar.gz
creates: archive.tar.gz
- name: Extract archive
shell: tar xzf archive.tar.gz
creates: archive/
Service management¶
- name: Enable service
shell: systemctl enable myservice
unless: "systemctl is-enabled myservice"
- name: Start service
shell: systemctl start myservice
unless: "systemctl is-active myservice"
Configuration management¶
- name: Update config
template:
src: config.j2
dest: /etc/myapp/config.yml
register: config_updated
- name: Restart service if config changed
shell: systemctl restart myapp
when: config_updated.changed
Database operations¶
- name: Create database
shell: createdb mydb
unless: "psql -l | grep mydb"
- name: Load schema
shell: psql mydb < schema.sql
unless: "psql mydb -c '\\dt' | grep users"
Troubleshooting¶
Step is not being skipped¶
-
Check file paths are correct:
-
Check command exit codes:
-
Use debug mode:
Step is being skipped incorrectly¶
- Verify the condition:
- For
creates: Is the file being deleted elsewhere? -
For
unless: Is the command returning the wrong exit code? -
Check for template variables:
See Also¶
- Configuration Reference - Full field documentation
- Control Flow - Conditionals and when expressions
- Variables - Template syntax and variables