Best Practices For Ansible
Best Practices For Ansible
for
Ansible
Introduction
What is Ansible?
● Install
● Configure reverse-proxy for an application
Name of things
Name of things
include_role import_role
inner outer action inner + outer inner action outer action inner + outer action
action action
INNER + OUTER hander outer only outer only outer only inner only inner only inner only
INNER handler only inner NOT FOUND NOT FOUND inner inner inner
https://github.com/amarao/ansible_import_include_and_handlers
Conditionals
● evaluated at the moment of execution
● Evaluated on every iteration for loops
● Separately for each entry in ‘block’
● Have a special hack for ‘is defined’
Loops
- All of them are slow and clumsy.
- Ansible 2.5: iter_items → loops.
- Complicated branching is bad.
- Complexity is bad.
loop_control:
loop_var: user
label: ‘{{user.short_name}} at {{user_department}}’
idempotency
● Each task or fail, or change something, or ‘success (no change)’, or skipped
● Each task should report change only if there are changes made.
● Second run of the same task should yield ‘no change’
Important for:
- Testing
- Stability and audit
- Handler’s calls
Ansible is not
a programming language.
ansible developer
What is ‘big’ means for an ansible project?
Kubespray
Openshift-ansible
● 911 files
● 49132 lines
● 1668 files
Openstack-ansible
● 175745 lines
● 1196 files
● 52504 lines
❌ NO ✅ YES
● add nginx configuration for foo ● Use wrapper role to configure
● use this magic query to find nginx (include_role, import_role)
database IP ● Use role to search database IP
● transform list of users from global ● Pass userlist explicitly from
userlist to foo format playbook or another wrapper role
Explicit dependencies
There is no the sane way to describe dependencies.
- Old style (with dependencies in meta) do not work and is been deprecating.
- New style include_role/import_role ignores meta-dependecies.
❌ NO ✅ YES
- name: Link up - name: Check link status
shell: | command: ip link show {{dev}}
ip link set up dev {{dev}} register: link_status
changed_when: False
- name: Link up
command: ip link set up dev {{dev}}
when: ‘UP’ not in link_status.stdout
shell example #2
foobar does not report failures at all.
We want to execute foobar add and we can to do foobar list .
❌ NO ✅ YES
MUST HAVE
STAGING
AT ANY COST
Development environment
Primary staging:
Development environment(s):
- Time consuming
- No inter-role integration
- Often meaningless without a context
Variables & environments
Places to hide a variable
● Inventory (host, group_name:vars)
● inventory/host_vars
● inventory/group_vars Ansible variables without supervision
● host_vars
● group_vars [all.yaml, group_name.yaml]
● roles/default
● roles/vars
● ‘vars:’ in any task or role
● register in any task
● import_vars
● defaults/vars of imported role
Rules to keep sanity
● host_vars are banned anywhere except an inventory
● Roles/vars should be avoided
● Roles should avoid to expose variables to other roles in the same play(book)
○ Reduce global state, OK?
○ If they do - this is called an ‘interface’. Document it.
■ Example: search-fo-database-ip can set a variable db_ip.
● Environment-specific variables are kept in the inventory
● Project-specific variables are kept in group_vars
● Roles should use defaults for rarely changed variables
● Use local ‘vars:’ statement for task-local calculations
Variables and environments
Environments:
roles/foo/tasks/.*.yaml (set_fact, external use outside of the role) 60+ minutes role and project docs
Mandatory!
For use in a command line (ansible-playbook -e) 60+ minutes role and project docs
Mandatory!
Assertions and validations
fail module with ‘when’ assert module
From ceph-ansible
Tags
Tags proliferation
- name: Configure foo
template: src=foo.conf.j2 dest=/etc/foo.conf
notify: restart foo
tags:
- foo
Tags proliferation
- name: Configure foo
template: src=foo.conf.j2 dest=/etc/foo.conf
notify: restart foo
tags:
- foo
- configure
Tags proliferation
- name: Configure foo
template: src=foo.conf.j2 dest=/etc/foo.conf
notify: restart foo
tags:
- foo
- configure
- restart
Tags proliferation
- name: Configure foo
become: yes
template: src=foo.conf.j2 dest=/etc/foo.conf
notify: restart foo
tags:
- foo
- configure
- restart
- become
Tags proliferation
- name: Configure foo
become: yes
template: src=foo.conf.j2 dest=/etc/foo.conf
notify: restart foo
tags:
- foo
- configure
- restart
- become
- ip
Tags proliferation
- name: Configure foo
become: yes
template: src=foo.conf.j2 dest=/etc/foo.conf
notify: restart foo
tags:
- foo
- configure
- restart
- become
- ip
- dont_do_like_this
Concise tags
Including tags: Excluding tags:
fatal: [host2]: FAILED! => {"changed": false, "msg": "dict has no element ansible_default_ipv4"}
Solutions
We need information about all hosts, but we have used --limit
Good: none
❌
Bad:
- incomplete config
- ‘changed’ for each time with different --limit
Lineinfile
- name: Add host to config
lineinfile: path=/etc/foo.conf line=”host {{(hostvars[item]).ansible_default_ipv4.address}}”
when: (hostvars[item]).ansible_default_ipv4 is defined
with_items: groups.all
Good:
Bad:
Good:
Bad:
Good:
- Works perfectly with --limit
✅
- Won’t fail if some host is down and --limit was used
- Fast
- Updates and removes old data as needed on each full run
Bad:
- Does not update config if --limit
templates
Template & task relationship
● Keep templates as simple as possible
● Use ‘vars:’ section for explicit variable declaration
● Never use global variables in a template. Exceptions:
○ Iterations over all hosts
○ Ansible built-in variables
○ A special global variable documented in a project and in a role
○ Very complicated queries. Use comments in the task to list used
variables inside the template.
Simplify
If a template is small, use ‘copy’ with ‘content’ argument to
inline it
- template:
dest: /etc/foobar.conf
content: |
source_ip = {{ansible_default_ipv4.address}}
Debugging templates: variables
- debug var={{item}}
with_items:
- myvar1
- myvar2
- ansible_default_ipv4
- all_other_variables_in_template
Debugging templates: Jinja2
Explicit templatization in a separate playbook (f.e. temp.yaml)
- template:
src=roles/somerole/templates/foo.conf.j2
dest: /tmp/foo.conf
delegate_to: localhost
transport: local
vars:
- some_var
- another_var
Templates everywhere
You don’t need to use ‘template’ to use jinja2. Every variable is a {{template}}.
- copy
- lineinfile
- blockinfile
- all file names for all copy/stat/file modules
- arguments to shell and command modules
- all other modules (apt, postgres_user, etc)
External Jinja2
- name: Ugly example
foo:
argument: ‘{{(hostvars[var1]).cust_facts[3]|json_query(“[?name=”+ ..
Bad:
- A tidy git.
Bad:
Examples
Role nginx configure any nginx site and it needs bunch of additional variables.
Wrapper role glues them together, but does not change anything in foo or nginx.
Wrapper role
- name: Configure foo for {{foo_source_ip}}
include_role: name=foo tasks_from=configure_foo
vars:
local_api_ip: '{{foo_local_ip}}'
local_api_port: '{{foo_local_port}}'
- Supports loops
- Absolute mess
- Broken in each new ansible release in a new way (hello, 2.5):
- Delegation
- Handlers
- Defaults vs set_fact
- Parent’s variable access
- include_tasks is much more reasonable, but requires more files and lines.
A proper looping with an include in a role
- name: Loop over something
Include_tasks: per_something.yaml
with_items: ‘{{something}}’
- lookup_plugins/
- Load data from external sources
- Perform calculations and queries
- Iterate
- action_plugins/
- Do stuff on hosts
- vars_plugins
- inventory_plugins
All plugins are written in Python, and can be stored in ‘*_plugins/’ directory near a
playbook, or within a role.
Lookup plugins
1. Try to do it with ansible.
2. Try to do it with in-line jinja2 template
3. Try to do it with in-line json_query
4. Try to do it with external jinja2_template
5. If not, write a plugin
Rule of thumb: if jinja2 template more then ⅓ of plugin (and it’s tests), write a
plugin. If less, use a jinja2.