Skip to content

Ansible Snippets

Ansible lint skip some rules

Add a .ansible-lint-ignore file with a line per rule to ignore with the syntax path/to/file rule_to_ignore.

Ansible retry a failed job

- command: /usr/bin/false
  retries: 3
  delay: 3
  register: result
  until: result.rc == 0

Ansible add a sleep

- name: Pause for 5 minutes to build app cache
  ansible.builtin.pause:
    minutes: 5

Ansible condition that uses a regexp

- name: Check if an instance name or hostname matches a regex pattern
  when: inventory_hostname is not match('molecule-.*')
  fail:
    msg: "not a molecule instance"

Ansible-lint doesn't find requirements

It may be because you're using requirements.yaml instead of requirements.yml. Create a temporal link from one file to the other, run the command and then remove the link.

It will work from then on even if you remove the link. ¯\(°_o)/¯

Run task only once

Add run_once: true on the task definition:

- name: Do a thing on the first host in a group.
  debug: 
    msg: "Yay only prints once"
  run_once: true

Run command on a working directory

- name: Change the working directory to somedir/ and run the command as db_owner 
  ansible.builtin.command: /usr/bin/make_database.sh db_user db_name
  become: yes
  become_user: db_owner
  args:
    chdir: somedir/
    creates: /path/to/database

Run handlers in the middle of the tasks file

If you need handlers to run before the end of the play, add a task to flush them using the meta module, which executes Ansible actions:

tasks:
  - name: Some tasks go here
    ansible.builtin.shell: ...

  - name: Flush handlers
    meta: flush_handlers

  - name: Some other tasks
    ansible.builtin.shell: ...

The meta: flush_handlers task triggers any handlers that have been notified at that point in the play.

Once handlers are executed, either automatically after each mentioned section or manually by the flush_handlers meta task, they can be notified and run again in later sections of the play.

Run command idempotently

- name: Register the runner in gitea
  become: true
  command: act_runner register --config config.yaml --no-interactive --instance {{ gitea_url }} --token {{ gitea_docker_runner_token }}
  args:
    creates: /var/lib/gitea_docker_runner/.runner

Get the correct architecture string

If you have an amd64 host you'll get x86_64, but sometimes you need the amd64 string. On those cases you can use the next snippet:

---
# vars/main.yaml
deb_architecture: 
  aarch64: arm64
  x86_64: amd64

---
# tasks/main.yaml
- name: Download the act runner binary
  become: True
  ansible.builtin.get_url:
    url: https://dl.gitea.com/act_runner/act_runner-linux-{{ deb_architecture[ansible_architecture] }}
    dest: /usr/bin/act_runner
    mode: '0755'

Check the instances that are going to be affected by playbook run

Useful to list the instances of a dynamic inventory

ansible-inventory -i aws_ec2.yaml --list

Check if variable is defined or empty

In Ansible playbooks, it is often a good practice to test if a variable exists and what is its value.

Particularity this helps to avoid different “VARIABLE IS NOT DEFINED” errors in Ansible playbooks.

In this context there are several useful tests that you can apply using Jinja2 filters in Ansible.

Check if Ansible variable is defined (exists)

tasks:

- shell: echo "The variable 'foo' is defined: '{{ foo }}'"
  when: foo is defined

- fail: msg="The variable 'bar' is not defined"
  when: bar is undefined

Check if Ansible variable is empty

tasks:

- fail: msg="The variable 'bar' is empty"
  when: bar|length == 0

- shell: echo "The variable 'foo' is not empty: '{{ foo }}'"
  when: foo|length > 0

Check if Ansible variable is defined and not empty

tasks:

- shell: echo "The variable 'foo' is defined and not empty"
  when: (foo is defined) and (foo|length > 0)

- fail: msg="The variable 'bar' is not defined or empty"
  when: (bar is not defined) or (bar|length == 0)

Start and enable a systemd service

Typically defined in handlers/main.yaml:

- name: Restart the service
  become: true
  systemd:
    name: zfs_exporter
    enabled: true
    daemon_reload: true
    state: started

And used in any task:

- name: Create the systemd service
  become: true
  template:
    src: service.j2
    dest: /etc/systemd/system/zfs_exporter.service
  notify: Restart the service

Download a file

- name: Download foo.conf
  ansible.builtin.get_url:
    url: http://example.com/path/file.conf
    dest: /etc/foo.conf
    mode: '0440'

Download an decompress a tar.gz

- name: Unarchive a file that needs to be downloaded (added in 2.0)
  ansible.builtin.unarchive:
    src: https://example.com/example.zip
    dest: /usr/local/bin
    remote_src: yes

If you want to only extract a file you can use the includes arg

- name: Download the zfs exporter
  become: true
  ansible.builtin.unarchive:
    src: https://github.com/pdf/zfs_exporter/releases/download/v{{ zfs_exporter_version }}/zfs_exporter-{{ zfs_exporter_version }}.linux-amd64.tar.gz
    dest: /usr/local/bin
    include: zfs_exporter
    remote_src: yes
    mode: 0755

But that snippet sometimes fail, you can alternatively download it locally and copy it:

- name: Test if zfs_exporter binary exists
  stat:
    path: /usr/local/bin/zfs_exporter
  register: zfs_exporter_binary

- name: Install the zfs exporter
  block:
    - name: Download the zfs exporter
      delegate_to: localhost
      ansible.builtin.unarchive:
        src: https://github.com/pdf/zfs_exporter/releases/download/v{{ zfs_exporter_version }}/zfs_exporter-{{ zfs_exporter_version }}.linux-amd64.tar.gz
        dest: /tmp/
        remote_src: yes

    - name: Upload the zfs exporter to the server
      become: true
      copy:
        src: /tmp/zfs_exporter-{{ zfs_exporter_version }}.linux-amd64/zfs_exporter
        dest: /usr/local/bin
        mode: 0755
  when: not zfs_exporter_binary.stat.exists

Skip ansible-lint for some tasks

- name: Modify permissions
  command: >
    chmod -R g-w /home/user
  tags:
    - skip_ansible_lint
  sudo: yes

Authorize an SSH key

- name: Authorize the sender ssh key
  authorized_key:
    user: syncoid
    state: present
    key: "{{ syncoid_receive_ssh_key }}"

Create a user

The following snippet creates a user with password login disabled.

- name: Create the syncoid user
  ansible.builtin.user:
    name: syncoid
    state: present
    password: !
    shell: /usr/sbin/nologin

If you don't set a password any user can do su your_user to set a random password use the next snippet:

- name: Create the syncoid user
  ansible.builtin.user:
    name: syncoid
    state: present
    password: "{{ lookup('password', '/dev/null', length=50, encrypt='sha512_crypt') }}"
    shell: /bin/bash

This won't pass the idempotence tests as it doesn't save the password anywhere (/dev/null) in the controler machine.

Create an ssh key

- name: Create .ssh directory
  become: true
  file:
    path: /root/.ssh
    state: directory
    mode: 700

- name: Create the SSH key to directory
  become: true
  openssh_keypair:
    path: /root/.ssh/id_ed25519
    type: ed25519
  register: ssh

- name: Show public key
  debug:
    var: ssh.public_key

Get the hosts of a dynamic ansible inventory

ansible-inventory -i environments/production --graph

You can also use the --list flag to get more info of the hosts.

Speed up the stat module

The stat module calculates the checksum and the md5 of the file in order to get the required data. If you just want to check if the file exists use:

- name: Verify swapfile status
  stat:
    path: "{{ common_swapfile_location }}"
    get_checksum: no
    get_md5: no
    get_mime: no
    get_attributes: no
  register: swap_status
  changed_when: not swap_status.stat.exists

Stop running docker containers

- name: Get running containers
  docker_host_info:
    containers: yes
  register: docker_info

- name: Stop running containers
  docker_container:
    name: "{{ item }}"
    state: stopped
  loop: "{{ docker_info.containers | map(attribute='Id') | list }}"

Moving a file remotely

Funnily enough, you can't without a command. You could use the copy module with:

- name: Copy files from foo to bar
  copy:
    remote_src: True
    src: /path/to/foo
    dest: /path/to/bar

- name: Remove old files foo
  file: path=/path/to/foo state=absent

But that doesn't move, it copies and removes, which is not the same.

To make the command idempotent you can use a stat task before.

- name: stat foo
  stat: 
    path: /path/to/foo
  register: foo_stat

- name: Move foo to bar
  command: mv /path/to/foo /path/to/bar
  when: foo_stat.stat.exists