Ansible Anti Patterns.
As a religous hackernews reader, I often read posts decrying the complexity of YAML based tooling, how various systems implementing a DSL (like ansible) shouldn’t include programming concepts like conditionals or loops. I agree with the sentiment that YAML should not be used like a programming language, even though flow control concepts like this are available.
The difficulty for many engineers, I think, is that they just haven’t seen the easier way yet. Years back I had the good fortune to work with a skilled engineer from Red Hat who specialized in ansible. What he told me seems obvious now, but it forever changed the way I manage my ansible configs.
Keep It Simple Stupid. (KISS)
Overall, playbooks should just describe what should happen and be declarative. Logic should be avoided in playbooks whenever possible.
Remembering KISS keeps playbooks simple, easy to read, and helps with developer confidence that your playbooks are safe.
Below I’ll give a few examples of what I’d consider Ansible Anti-Patterns, and show the easier way to handle things.
1. Conditional Configuration
This is a simple one to fix. I frequently see “reconfigure flags”
- name: Copy these files
win_copy:
src: appsettings.json
dest: C:/foo/appsettings.json
when: "reconfigure is true"
The better way…
Remember that modules like copy
or template
only return “changed” if the checksum of the destination file doesn’t match the file or template you’re trying to copy over.
The more variables you have in a role or playbook, the more you’ll have to test against and remember when executing. Keep it simple, and use built in module arguments.
2. Conditional Actions
Frequently, I’ll see blocks like the below in custom roles
.
Playbook developers insert variables to allow for condintional execution of tasks like restarts or reboots.
- name: Restart the service
service:
name: foobar
state: restarted
when: "destructive is true"
The better way…
It’s understandable that the playbook developer is trying to prevent unintended downtime. In general, I make two recommendations to clients here.
a) If you occasionally just need to “restart” services or perform one off actions, do so separetly from your role. It’s an ad-hoc operation, not part of your configuration.
ansible -i inventory app_servers -m win_service -a 'name=app state=restarted'
b) If you need to restart services as a part of your configuration, leverage handlers so that you only restart when required.
# foo_role/tasks/main.yml
---
- name: Copy the config
win_copy:
src: appsettings.json
dest: C:/foo/appsettings.json
notify:
- Restart foo Service
# foo_role/handlers/main.yml
---
- name: Restart foo Service
win_service:
name: foo
state: restarted
3. Conditional Installs
I find this most often in windows playbooks. Users might have an old .exe
or perhaps custom installation files, and they are unsure of how to leverage ansible’s built in idempotency
- name: Install packages that we need.
win_package:
path: foo_v1.exe
arguments:
- /install
- /passive
- /norestart
when: "first_run is true or upgrade is true"
It’s very tempting to add this conditional. Users usually see multiple “changed” runs, or notice the foo.exe
setup program executes on every playbook run, so they add a flag to disable it from running.
The better way…
Use built in methods to detect if your application is already installed.
Example: Use product ID to detect if the application has a registered win32_productid
.
- name: Install packages that we need.
win_package:
path: foo_v1.exe
product_id: '{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}'
arguments:
- /install
- /passive
- /norestart
Often, the productid isn’t available. You can also use
- creates_path to check for an
.exe
in program files. - creates_s_ervices to check for a
service
that should exist post installation. - creates_version to check the version of the
.exe
. This allows you to only upgrade when the version flag on the exe has changed.
Several modules implement checks like the above, so that action is only taken when required. The below examples are for windows, but their linux counterparts implement the same.
- win_get_url checksums.
- win_shell for arbitrary shell execution.
Concepts like these, written into complex playbooks result in behaviour that’s borderline undefined. “Why is my playbook doing that?”, or “Why was this task skipped?”
4. Conditional “Hosts”
I sometimes see users checking group membership within a role.
- name: Install prod sidekick service
yum:
name: additional_service_sidekick.deb
when: "prod in ['group_names']"
The better way…
There’s two possibilities here, but generally, the expectation when running a role, is that all tasks will run on all targeted hosts. Conditional logic like this better left to playbook execution and grouping.
a) Change the task to leverage group variables.
# group_vars/prod.yaml
---
foobar_debs:
- main.deb
- sidekick.deb
# group_vars/dev.yaml
---
foobar_debs:
- main.deb
# roles/foo_service/tasks/main.yaml
---
- name: Install services
apt:
deb: "{{item }}"
with_items: "{{foobar_debs}}"
b) Create a second role. (My Recommendation)
# site.yml
---
- hosts: "foo_service&prod"
roles:
- foo_service
- foo_sidekick_service
- hosts: "foo_service&dev"
roles:
- foo_service