Red Hat Certified Engineer (EX294) Sample Exam
- EN
- ES
Table of Contents
I think it was in late 2019 when Red Hat updated their RHCE exam, now based on RHEL 8 and the Ansible Automation Platform. The performance-based exam code is EX294 (more information here) and by passing this exam you become a Red Hat Certified Engineer.
There are not too many EX294 mock exams and the best I have found is this one by lisenet.com. Thanks, Tomas!
Of course there are many ways to solve it, but this is mine.
##
Lisenet: Ansible Sample Exam for RHCE EX294
The original site here -> https://www.lisenet.com/2019/ansible-sample-exam-for-ex294/. Please visit the site to read the complete assignments.
#
Preparation
First of all I created 5 RHEL 8 virtual machines. Remember you can get free developer RHEL 8 subscriptions. CentOS 8 / Rocky Linux 8 should also work. Machines are:
- ansible-control.hl.local
- ansible2.hl.local
- ansible3.hl.local
- ansible4.hl.local
- ansible5.hl.local
I used 1 vCPU, 2 GB memory and 20 GB thin provisioned disks. ansible5 needs an extra 1 GB Disk for some tasks. I assigned static IP addresses to all of them and added everything to the /etc/hosts
file.
If RHEL 8 is chosen remember you have to register the systems:
# subscription-manager register --username <username> --password <password> --force
# subscription-manager attach
In the control machine you need some additional steps to install Ansible:
# subscription-manager repos --enable ansible-2-for-rhel-8-x86_64-rpms
# yum -y install ansible
#
Task 1: Ansible Installation and Configuration
First tasks is to manually setup the environment: create automation user in ansible-control and configure new ansible defaults:
- The roles path should include
/home/automation/plays/roles
, as well as any other path that may be required for the course of the sample exam. - The inventory file path is /home/automation/plays/inventory.
- Privilege escalation is disabled by default.
- Ansible should be able to manage 10 hosts at a single time.
- Ansible should connect to all managed nodes using the automation user.
Our working directory will be /home/automation/plays
I created /home/automation/plays/ansible.cfg
with this content:
[defaults]
roles_path = /home/automation/plays/roles
inventory = /home/automation/plays/inventory
remote_user = automation
forks = 10
[privilege_escalation]
become = false
And the inventory file:
[proxy]
ansible2.hl.local
[webservers]
ansible3.hl.local
ansible4.hl.local
[database]
ansible5.hl.local
#
Task 2: Ad-Hoc Commands
For this task we have to use ad-hoc commands to prepare the remaining machines to be properly used via ansible. This is my script:
#!/bin/bash
# create user automation
ansible all -u root --ask-pass -m user -a "name=automation state=present"
# create .ssh directory
ansible all -u root --ask-pass -m file -a "path=/home/automation/.ssh state=directory owner=automation group=automation mode=0700"
# copy id_rsa.pub
ansible all -u root --ask-pass -m copy -a "src=/home/automation/.ssh/id_rsa.pub dest=/home/automation/.ssh/authorized_keys owner=automation group=automation mode=0600"
# add sudo permission
ansible all -u root --ask-pass -m copy -a "content='automation ALL=(ALL) NOPASSWD: ALL' dest=/etc/sudoers.d/automation owner=root group=root mode=0600"
#
Task 3: File Content
I used the magic variable inventory_hostname
to match the conditions:
---
- name: task 3
hosts: all
become: yes
tasks:
- name: copy content to HAProxy
copy:
content: "Welcome to HAProxy server"
dest: /etc/motd
when: inventory_hostname in groups["proxy"]
- name: copy content to webservers
copy:
content: "Welcome to Apache server"
dest: /etc/motd
when: inventory_hostname in groups["webservers"]
- name: copy content to Database
copy:
content: "Welcome to MySQL server"
dest: /etc/motd
when: inventory_hostname in groups["database"]
#
Task 4: Configure SSH Server
---
- name: task 4
hosts: all
become: yes
tasks:
- name: configure sshd daemon
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^Banner"
line: Banner /etc/motd
- name: disable X11Forwarding
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^X11Forwarding"
line: X11Forwarding no
- name: set MaxAuthTries = 3
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^MaxAuthTries"
line: MaxAuthTries 3
- name: restart ssh server
service:
name: sshd
state: restarted
enabled: yes
#
Task 5: Ansible Vault
Ansible vault commands are needed here. For example: ansible-vault view secret.yml --vault-password-file vault_key
. At the end of the exercise you must have two files:
secret.yml
: encrypted file with credentialsvault_key
: the encryption key
#
Task 6: Users and Groups
We will have to loop through a list of users:
---
users:
- username: alice
uid: 1201
- username: vincent
uid: 1202
- username: sandy
uid: 2201
- username: patrick
uid: 2202
This is my playbook:
---
- name: task 6
hosts: all
become: yes
vars_files:
- ./vars/user_list.yml
- ./secret.yml
tasks:
- name: ensure group wheel exists
group:
name: wheel
state: present
- name: create users in webservers group
loop: "{{ users }}"
user:
name: "{{ item.username }}"
password: "{{ user_password | password_hash('sha512')}}"
update_password: on_create
groups: wheel
shell: /bin/bash
when:
- inventory_hostname in groups['webservers']
- item.uid | string | first == "1"
- name: create users in database group
loop: "{{ users }}"
user:
name: "{{ item.username }}"
password: "{{ user_password | password_hash('sha512')}}"
update_password: on_create
groups: wheel
shell: /bin/bash
when:
- inventory_hostname in groups['database']
- item.uid | string | first == "2"
- name: create .ssh directory (webservers)
loop: "{{ users }}"
file:
name: "/home/{{ item.username }}/.ssh"
state: directory
owner: "{{ item.username }}"
group: "{{ item.username }}"
mode: 0700
when:
- inventory_hostname in groups['webservers']
- item.uid | string | first == "1"
- name: create .ssh directory (database)
loop: "{{ users }}"
file:
name: "/home/{{ item.username }}/.ssh"
state: directory
owner: "{{ item.username }}"
group: "{{ item.username }}"
mode: 0700
when:
- inventory_hostname in groups['database']
- item.uid | string | first == "2"
- name: copy ssh authorized key (webservers)
loop: "{{ users }}"
copy:
src: "/home/automation/.ssh/id_rsa.pub"
dest: "/home/{{ item.username }}/.ssh/authorized_keys"
owner: "{{ item.username }}"
group: "{{ item.username }}"
mode: 0600
when:
- inventory_hostname in groups['webservers']
- item.uid|string|first == "1"
- name: copy ssh authorized key (database)
loop: "{{ users }}"
copy:
src: "/home/automation/.ssh/id_rsa.pub"
dest: "/home/{{ item.username }}/.ssh/authorized_keys"
owner: "{{ item.username }}"
group: "{{ item.username }}"
mode: 0600
when:
- inventory_hostname in groups['database']
- item.uid|string|first == "2"
Ths playbook is run as:
ansible-playbook users.yml --vault-password-file vault_key
It took some time to figure out how to do the “starts with ‘1’” thingy. Turns out that it was really much simpler using just:
item.uid < 2000
item.uid >= 2000
#
Task 7: Scheduled Tasks
I chose to use a md5 hash for the job name because it looks fancy to me (and I wanted to learn how to do it). But it is not a requirement and a descriptive name could be more appropriate depending on the environment.
---
- name: task 7
hosts: proxy
become: yes
tasks:
- name: create a cron job with a fancy name on proxy hosts
cron:
name: "{{ 'proxy: append date to time.log' | hash('md5') }}"
minute: "0"
job: "date >> /var/log/time.log"
#
Task 8: Software Repositories
I did not know wich module to use so I run:
ansible-doc -l|grep yum
yum Manages packages with the `yum' package manager
yum_repository Add or remove YUM repositories
to find it. This is my possible solution:
---
- name: task 8
hosts: database
become: yes
tasks:
- name: create yum repository
yum_repository:
name: "mysql80-community"
baseurl: http://repo.mysql.com/yum/mysql-8.0-community/el/8/x86_64/
description: "MySQL 8.0 YUM Repo"
enabled: true
gpgkey: http://repo.mysql.com/RPM-GPG-KEY-mysql
gpgcheck: true
#
Task 9: Create and Work with Roles
To create the role template:
cd roles
ansible-galaxy role init --offline sample-mysql
The playbook:
---
- name: task 9
hosts: database
become: true
vars_files:
- secret.yml
roles:
- sample-mysql
The role file:
---
# tasks file for sample-mysql
I spent quite some time in this one. First of all, `mysql-community-server` is not available in the configure repos, so I used `mysql-server` instead. But it took some time to figure out that `mysql` and `python3-PyMySQL` are also required.
- name: create primary partition
parted:
device: /dev/nvme0n2
number: 1
flags: [ lvm ]
state: present
part_end: 800MB
- name: create VG vg_database using the primary partition created above
lvg:
vg: vg_database
pvs: /dev/nvme0n2p1
- name: create LV lv_mysql size 512MB in the VG vg_database
lvol:
vg: vg_database
lv: lv_mysql
size: 512m
- name: create an XFS filesystem on lv_mysql
filesystem:
fstype: xfs
dev: /dev/vg_database/lv_mysql
- name: ensure mount point /mnt/mysql_backups exists
file:
path: /mnt/mysql_backups
state: directory
owner: root
group: root
mode: 775
- name: permanently mount filesystem
mount:
path: /mnt/mysql_backups
src: /dev/vg_database/lv_mysql
fstype: xfs
state: mounted
- name: install mysql-server
yum:
name: "{{ item }}"
state: latest
loop:
- mysql-server
- mysql
- python3-PyMySQL
- name: allow mysql traffic
firewalld:
service: mysql
permanent: true
immediate: true
state: enabled
- name: start and enable mysql
service:
name: mysqld
state: started
enabled: true
- name: configure root user
mysql_user:
check_implicit_admin: true
login_host: localhost
login_user: root
login_password: ''
name: root
password: "{{ database_password }}"
state: present
update_password: always
# no_log: true
- name: deploy configuration
template:
src: mysql.j2
dest: /etc/my.cnf
owner: root
group: root
mode: 0644
#
Task 10: Create and Work with Roles (Some More)
The playbook:
---
- name: task 10
hosts: webservers
become: true
roles:
- sample-apache
The role:
---
# tasks file for roles/sample-apache
- name: install apache
yum:
name: "{{ item }}"
state: latest
loop:
- httpd
- mod_ssl
- php
- name: allow incoming http/https traffic
firewalld:
service: "{{ item }}"
immediate: true
permanent: true
state: enabled
loop:
- http
- https
- name: start and enable the apache service
service:
name: httpd
state: started
enabled: true
- name: update index.html
template:
src: index.html.j2
dest: /var/www/html/index.html
owner: root
group: root
mode: 0644
notify: restart apache
The handlers file:
---
# handlers file for roles/sample-apache
- name: restart apache
service:
name: httpd
state: restarted
#
Task 11: Download Roles From Ansible Galaxy and Use Them
Install the role:
ansible-galaxy install geerlingguy.haproxy
The playbook:
---
- name: task 11
hosts: proxy
become: true
vars:
haproxy_backend_servers:
- name: ansible3
address: 172.16.10.203:80
- name: ansible4
address: 172.16.10.204:80
haproxy_backend_balance_method: 'roundrobin'
haproxy_backend_mode: 'http'
roles:
- geerlingguy.haproxy
tasks:
- name: enable http traffic to proxy
firewalld:
service: http
state: enabled
immediate: true
permanent: true
Note: it may be a good idea to add firewalld installation and configuration just in case.
#
Task 12: Security
Install roles:
yum install rhel-system-roles
Since the installed roles are not in the path, the ansible.cfg has to be modified:
[defaults]
roles_path = /home/automation/plays/roles:/usr/share/ansible/roles
[...]
The playbook:
---
- name: task 12
hosts: webservers
become: true
vars:
- selinux_booleans:
- name: httpd_can_network_connect
state: on
persistent: true
roles:
- rhel-system-roles.selinux
#
Task 13: Use Conditionals to Control Play Execution
To find the fact:
ansible ansible2.hl.local -m setup | grep -A10 memory
[...]
"ansible_memory_mb": {
"nocache": {
"free": 1447,
"used": 343
},
"real": {
"free": 961,
"total": 1790,
"used": 829
},
"swap": {
[...]
So the variable to use is ansible_memory_mb.real.total
. And the playbook:
---
- name: task 13
hosts: all
become: true
tasks:
- name: set vm.swappiness to 10 if server has 2GB memory
sysctl:
name: vm.swappiness
value: 10
state: present
when:
- ansible_memory_mb.real.total >= 2048
- name: report not enough total memory
debug:
msg: "Server memory less than 2048MB ({{ ansible_memory_mb.real.total }}MB)"
when:
- ansible_memory_mb.real.total < 2048
#
Task 14: Use Archiving
---
- name: task 14
hosts: database
become: true
tasks:
- name: create database list file
copy:
content: "dev,test,qa,prod"
dest: /mnt/mysql_backups/database_list.txt
- name: archive file
archive:
path: /mnt/mysql_backups/database_list.txt
dest: /mnt/mysql_backups/archive.gz
format: "gz"
#
Task 15: Work with Ansible Facts
The file gets created but çi am not able to get the custom facts this way:
ansible ansible5.hl.local -m setup -a "filter=ansible_local"
ansible5.hl.local | SUCCESS => {
"ansible_facts": {
"ansible_local": {},
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false
}
As you can see the ansible_local
variable is empty. However this works:
- name: test
debug:
msg: "{{ ansible_local }}"
and returns the custom fact:
ok: [ansible5.hl.local] => {
"msg": {
"custom": {
"sample_exam": {
"server_role": "mysql"
}
}
}
}
I am not really sure if this is the normal behaviour. In any case this is my (maybe wrong) playbook:
---
- name: task 15
hosts: database
become: true
tasks:
- name: ensure facts directory exists
file:
path: /etc/ansible/facts.d
state: directory
recurse: true
- name: create custom fact
copy:
content: "[sample_exam]\nserver_role = mysql\n"
dest: /etc/ansible/facts.d/custom.fact
#
Task 16: Software Packages
---
- name: task 16
hosts: all
become: true
tasks:
- name: install software in proxy group
yum:
name: "{{ item }}"
state: latest
loop:
- tcpdump
- mailx
when:
- inventory_hostname in groups['proxy']
- name: install software in database group
yum:
name: "{{ item }}"
state: latest
loop:
- lsof
- mailx
when:
- inventory_hostname in groups['database']
#
Task 17: Services
There is no module that I am aware of to accomplish this. You need to know a bit about systemd internals but it is as easy as creating a soft link.
---
- name: task 17
hosts: webservers
become: true
tasks:
- name: set default target to multi-user
file:
src: /usr/lib/systemd/system/multi-user.target
dest: /etc/systemd/system/default.target
state: link
#
Task 18. Create and Use Templates to Create Customised Configuration Files
The playbook:
---
- name: task 18
hosts: database
become: true
tasks:
- name: deploy server list
template:
src: server_list.j2
dest: /etc/server_list.txt
owner: automation
group: automation
mode: 0600
setype: net_conf_t
The template:
{% for host in groups["all"] %}
{{ hostvars[host]['inventory_hostname'] }}
{% endfor %}
#
Tips & tricks
- All the code available in Bitbucket: https://bitbucket.org/ins3cure/ex294
- As far as I know, documentation in docs.ansible.com is locally available during the exam. Command
ansible-doc
is also available and I find it quite helpful. Just runansible-doc -l | grep <foobar>
to search for the module you need. - Interesting resource: https://redhat-certs.slack.com
- Avoid WiFi networks for the exam. Yes, I know it should work fine, but…
- Make sure you run the system requirements in advance. See https://www.redhat.com/en/resources/remote-exams-preparation-ebook for details. DO NOT WAIT UNTIL THE LAST MOMENT.