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.
ansibleansible-playbookansible-docansible-doc -lansible-doc -sansible-inventory--check--diff--syntax-check-bansible.builtin.lineinfileansible.builtin.templateansible.builtin.dnf
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
| Part | What it is |
|---|---|
| Inventory | The list of managed hosts and groups ([lab]), a file in INI or YAML form |
| Module | A unit of work with parameters: copy, lineinfile, dnf, service, user, file, template |
| Task | One module call with a name |
| Play | Tasks applied to a group of hosts, with options such as become and vars |
| Playbook | A YAML file with one or more plays |
| Handler | A task that runs at the end of the play, only if a task notified it and reported changed |
| Fact | A 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
| Symptom | Likely 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 runs | notify: text and handler name: differ (case matters) |
Permission denied writing under /etc | The 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 run | A command/shell task without creates:, or a template that writes a timestamp |
Failed to connect ... python | Managed 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.
- Warm-up
Answer from ansible-doc
Without a browser, find: (1) the module that manages one line in a text file and its
regexpparameter; (2) whether a module for firewalld rules is installed; (3) the example in the documentation for creating a directory with mode0750.Check your result
- (1)
ansible-doc ansible.builtin.lineinfileshowsregexp. - (2)
ansible-doc -l | grep -i firewalldlists a module, or lists nothing (then the collection is not installed, and you know to usefirewall-cmdinstead). - (3)
ansible-doc ansible.builtin.filehas anEXAMPLESsection with astate: directorytask.
Hint
ansible-doc -llists every module; add| grep word.ansible-doc -s MODULEprints a short snippet of the parameters. The full page ends withEXAMPLES.Solution
ansible-doc ansible.builtin.lineinfile | sed -n 1,40p ansible-doc -l | grep -i firewalld ansible-doc ansible.builtin.file | grep -A12 EXAMPLES - (1)
- Practice
Distribute a forwarding rule
Write
forward.ymlfor the inventory grouplab. It must (a) copy a one-line rsyslog rule to/etc/rsyslog.d/forward.confusing a variableloghost, owner root, mode0644; (b) restart rsyslog only when the file changed. Run it with--check --diff, then for real, then a second time. The second run must reportchanged=0.Check your result
ansible-playbook forward.yml --syntax-checkprints the playbook name only.- The first real run shows the handler running; the second ends with
changed=0. rsyslogd -N1on the managed host validates the result.
Hint
A handler is a task in a
handlers:section that runs when a task withnotify:reportschanged. The names must match exactly. Usebecome: truebecause 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: restartedansible-playbook forward.yml --syntax-check ansible-playbook forward.yml --check --diff ansible-playbook forward.yml ansible-playbook forward.yml | tail -4 - 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: restartedFind and fix every mistake using the tool output, not by reading alone. The end state:
--syntax-checkpasses, the run succeeds withbecome, sshd restarts only when a setting changed, and a second run showschanged=0.Check your result
ansible-playbook broken.yml --syntax-checkpasses.- 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'showsnoand3.
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/sshneedsbecome: true, and the secondlineinfileneedscreateor the file to exist from the first task. Checksshd -tafter 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: restartedansible-playbook broken.yml --syntax-check ansible-playbook broken.yml ansible-playbook broken.yml | tail -4 sudo sshd -T | grep -E 'permitrootlogin|maxauthtries'