3 of 3

Central Logging, AIDE and AnsibleLesson 3 of 3

Ansible for repeatable fixes and checks

Use ansible-doc, ad hoc commands and a playbook with handlers and check mode to apply and verify a configuration.

  • ansible
  • ansible-playbook
  • ansible-doc
  • ansible-doc -l
  • ansible-doc -s
  • ansible-inventory
  • --check
  • --diff
  • --syntax-check
  • -b
  • ansible.builtin.lineinfile
  • ansible.builtin.template
  • ansible.builtin.dnf

50 min3 exercises

After this lesson you can

  • Find a module and its parameters with ansible-doc, offline.
  • Run ad hoc commands against an inventory and recognise changed versus unchanged results.
  • Write a playbook with variables and a handler, preview it with --check --diff, and debug a broken one.

The idea

When the same fix has to be applied to several machines, or applied again after you rebuilt one, typing commands does not scale and cannot be reviewed. Ansible describes the desired state in a text file and brings each machine to it. It connects over SSH, needs no agent on the managed hosts (only Python), and is built so that running it twice changes nothing the second time. That property is idempotence, and it is why you can run a playbook as a check.

On RHEL the control tools come from the ansible-core package in AppStream. Check what you have with ansible --version. The command set below is the same across versions.

The parts

PartWhat it is
InventoryThe list of managed hosts and groups ([lab]), a file in INI or YAML form
ModuleA unit of work with parameters: copy, lineinfile, dnf, service, user, file, template
TaskOne module call with a name
PlayTasks applied to a group of hosts, with options such as become and vars
PlaybookA YAML file with one or more plays
HandlerA task that runs at the end of the play, only if a task notified it and reported changed
FactA variable Ansible gathers about the host, such as the distribution or addresses

Prefer the full module names (ansible.builtin.copy). They avoid ambiguity and let ansible-doc find the page. Only command, shell and raw are not idempotent by themselves; if you must use command, add creates: or removes: so it skips when the work is already done.

Configuration and access

Ansible reads settings from ansible.cfg (current directory, then ~/.ansible.cfg, then /etc/ansible/ansible.cfg). A minimal project directory has an ansible.cfg naming the inventory, the remote user and privilege escalation:

[defaults]
inventory = ./inventory
remote_user = alex

[privilege_escalation]
become = true

Key-based SSH to the managed hosts and sudo for the remote user make it work without prompts. If it asks for a password or says Permission denied, you are debugging SSH, not Ansible: try ssh alex@node2.lab.example first.

Try it

Real output from ansible-core, on a local connection. Start with documentation, which is available offline.

ansible-doc -l | wc -l
ansible-doc -s ansible.builtin.lineinfile | sed -n 1,4p

ansible-doc -l counts the installed modules (71 in this test environment; a RHEL host with extra collections shows more, and which ones depend on the packages you installed). -s shows a snippet of every parameter. The full page without -s ends with working EXAMPLES, which is what you copy during an exam.

Ad hoc commands

An ad hoc command runs one module on a group, with no playbook:

ansible lab -i inventory -m ansible.builtin.copy -a "dest=/tmp/ans/x.txt content=hello"
node1 | CHANGED => {
    "changed": true,
    "checksum": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d",
    "dest": "/tmp/ans/x.txt",
    "mode": "0644",
    ...

Run the identical command again:

node1 | SUCCESS => {
    "changed": false,
    "checksum": "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d",
    "dest": "/tmp/ans/x.txt",
    ...

CHANGED the first time, SUCCESS with "changed": false the second: the file already matched, so nothing happened. This is the idempotent behaviour you rely on.

Useful ad hoc commands while troubleshooting: ansible all -m ansible.builtin.ping (SSH and Python work), ansible lab -m ansible.builtin.setup -a 'filter=ansible_distribution*' (facts), ansible lab -b -m ansible.builtin.command -a 'systemctl is-active rsyslog' (run a read-only check everywhere; -b means become).

A playbook with a handler

---
- name: Forward logs to the central host
  hosts: lab
  vars:
    loghost: loghost.lab.example
  tasks:
    - name: Install the forwarding rule
      ansible.builtin.copy:
        dest: /tmp/ans/rs.conf
        content: |
          *.* action(type="omfwd" target="{{ loghost }}" port="514" protocol="tcp")
        mode: "0644"
      notify: Restart rsyslog
  handlers:
    - name: Restart rsyslog
      ansible.builtin.debug:
        msg: "systemctl restart rsyslog would run here"
ansible-playbook -i inventory play.yml
PLAY [Forward logs to the central host] ****************************************

TASK [Gathering Facts] *********************************************************
ok: [node1]

TASK [Install the forwarding rule] *********************************************
changed: [node1]

RUNNING HANDLER [Restart rsyslog] **********************************************
ok: [node1] => {
    "msg": "systemctl restart rsyslog would run here"
}

PLAY RECAP *********************************************************************
node1                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

(The handler uses debug here only so the test does not touch a service; in your playbook use ansible.builtin.service with state: restarted.) The second run:

TASK [Install the forwarding rule] *********************************************
ok: [node1]

PLAY RECAP *********************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

No changed, so no notification, so no handler. The service is not restarted needlessly. Always read the PLAY RECAP: changed=0 on the second run proves idempotence; failed or unreachable is non-zero when something broke.

Look before you change

--check runs without changing anything, and --diff shows what would change in text files:

ansible-playbook -i inventory fix.yml --check --diff
TASK [Root login off] **********************************************************
--- before: /tmp/ans/sshd_test.conf (content)
+++ after: /tmp/ans/sshd_test.conf (content)
@@ -1,2 +1,2 @@
-#PermitRootLogin yes
+PermitRootLogin no
 Port 22

changed: [node1]
ansible-playbook -i inventory fix.yml --syntax-check
playbook: fix.yml

--syntax-check only parses the YAML and the structure; it does not contact hosts and cannot see a wrong parameter or a missing privilege. --check can, but some modules (command, shell) cannot predict their result and are skipped.

The mistakes you will meet

SymptomLikely cause
YAML syntax: mapping values are not allowed here or could not find expected ':'Indentation or a missing colon; YAML allows spaces, never tabs
The requested handler 'x' was not found / handler never runsnotify: text and handler name: differ (case matters)
Permission denied writing under /etcThe play lacks become: true (or -b)
couldn't resolve module/action 'ansible.posix.firewalld'The collection is not installed: ansible-doc -l | grep firewalld shows it
UNREACHABLE! ... Permission denied (publickey)SSH keys, user name or ansible_user
changed every runA command/shell task without creates:, or a template that writes a timestamp
Failed to connect ... pythonManaged host has no Python interpreter at the discovered path

Going further

Roles from Red Hat. The rhel-system-roles package supplies tested roles for common jobs (time synchronisation, SELinux, firewall, logging, storage, network). Reading how they set a variable is a good source of working examples, and they are the supported way to automate these settings in production. Beyond the exam: keep playbooks in version control, use ansible-vault for secrets, and test with a second host before the real one.

Practice

0 of 3 done

Do these on your own system. They get harder. Check each result, then tick it off.

  1. Warm-up

    Answer from ansible-doc

    Without a browser, find: (1) the module that manages one line in a text file and its regexp parameter; (2) whether a module for firewalld rules is installed; (3) the example in the documentation for creating a directory with mode 0750.

    Check your result
    • (1) ansible-doc ansible.builtin.lineinfile shows regexp.
    • (2) ansible-doc -l | grep -i firewalld lists a module, or lists nothing (then the collection is not installed, and you know to use firewall-cmd instead).
    • (3) ansible-doc ansible.builtin.file has an EXAMPLES section with a state: directory task.
    Hint

    ansible-doc -l lists every module; add | grep word. ansible-doc -s MODULE prints a short snippet of the parameters. The full page ends with EXAMPLES.

    Solution
    ansible-doc ansible.builtin.lineinfile | sed -n 1,40p
    ansible-doc -l | grep -i firewalld
    ansible-doc ansible.builtin.file | grep -A12 EXAMPLES
  2. Practice

    Distribute a forwarding rule

    Write forward.yml for the inventory group lab. It must (a) copy a one-line rsyslog rule to /etc/rsyslog.d/forward.conf using a variable loghost, owner root, mode 0644; (b) restart rsyslog only when the file changed. Run it with --check --diff, then for real, then a second time. The second run must report changed=0.

    Check your result
    • ansible-playbook forward.yml --syntax-check prints the playbook name only.
    • The first real run shows the handler running; the second ends with changed=0.
    • rsyslogd -N1 on the managed host validates the result.
    Hint

    A handler is a task in a handlers: section that runs when a task with notify: reports changed. The names must match exactly. Use become: true because you write to /etc.

    Solution
    ---
    - name: Forward logs to the central host
      hosts: lab
      become: true
      vars:
        loghost: node2.lab.example
      tasks:
        - name: Install the forwarding rule
          ansible.builtin.copy:
            dest: /etc/rsyslog.d/forward.conf
            content: |
              *.* action(type="omfwd" target="{{ loghost }}" port="514" protocol="tcp")
            owner: root
            group: root
            mode: "0644"
          notify: Restart rsyslog
      handlers:
        - name: Restart rsyslog
          ansible.builtin.service:
            name: rsyslog
            state: restarted
    ansible-playbook forward.yml --syntax-check
    ansible-playbook forward.yml --check --diff
    ansible-playbook forward.yml
    ansible-playbook forward.yml | tail -4
  3. Challenge

    Repair a broken playbook

    Setup: save this as broken.yml (it has four mistakes) and an inventory with one host.

    ---
    - name: Harden and report
      hosts: lab
      tasks:
        - name: Root login off
          ansible.builtin.lineinfile:
            path: /etc/ssh/sshd_config.d/01-local.conf
            line: PermitRootLogin no
            create: true
        - name: Limit tries
          ansible.builtin.lineinfile
            path: /etc/ssh/sshd_config.d/01-local.conf
            line: MaxAuthTries 3
          notify: restart sshd
        - name: Install a log helper
          ansible.builtin.dnf:
            name: rsyslog
            state: present
      handlers:
        - name: Restart sshd
          ansible.builtin.service:
            name: sshd
            state: restarted

    Find and fix every mistake using the tool output, not by reading alone. The end state: --syntax-check passes, the run succeeds with become, sshd restarts only when a setting changed, and a second run shows changed=0.

    Check your result
    • ansible-playbook broken.yml --syntax-check passes.
    • The first run changes the file and restarts sshd (handler runs); the second run ends with changed=0.
    • sudo sshd -T | grep -E 'permitrootlogin|maxauthtries' shows no and 3.
    Hint

    The syntax checker finds two of them: a missing colon after the module name, and the handler name that does not match notify (it is case-sensitive). The other two need running: writing in /etc/ssh needs become: true, and the second lineinfile needs create or the file to exist from the first task. Check sshd -t after any change.

    Solution
    ---
    - name: Harden and report
      hosts: lab
      become: true
      tasks:
        - name: Root login off
          ansible.builtin.lineinfile:
            path: /etc/ssh/sshd_config.d/01-local.conf
            line: PermitRootLogin no
            create: true
            mode: "0600"
          notify: Restart sshd
        - name: Limit tries
          ansible.builtin.lineinfile:
            path: /etc/ssh/sshd_config.d/01-local.conf
            line: MaxAuthTries 3
          notify: Restart sshd
        - name: Install a log helper
          ansible.builtin.dnf:
            name: rsyslog
            state: present
      handlers:
        - name: Restart sshd
          ansible.builtin.service:
            name: sshd
            state: restarted
    ansible-playbook broken.yml --syntax-check
    ansible-playbook broken.yml
    ansible-playbook broken.yml | tail -4
    sudo sshd -T | grep -E 'permitrootlogin|maxauthtries'