Red Hat Certified Engineer (RHCE) #14 RHCSA Automation 1: Users/Groups, Packages/Repositories
Through #13 system roles we covered all of Ansible’s syntax and structuring tools. From here, the next four posts are the synthesis stretch where we wield those tools to automate RHCSA’s manual work as playbooks. Roughly half of the RHCE exam’s weight is this “automate RHCSA tasks with Ansible,” so from here on is essentially the main event.
In this post, the work you did by hand in RHCSA with useradd, groupadd, and dnf install we convert into the user, group, dnf, and yum_repository modules and handle idempotently as playbook tasks. If you’re curious about the manual side, checking RHCSA #8 Users and Groups and RHCSA #11 Packages and Repositories first makes the target of automation clearer.
group module: create the group first #
To put a user into a group, the group has to exist first. The job you did in RHCSA with groupadd developers we handle with the ansible.builtin.group module.
- name: Ensure the developers group
ansible.builtin.group:
name: developers
gid: 5000
state: presentThe main options are simple.
| Option | Meaning |
|---|---|
name | Group name (required) |
gid | GID to assign. If omitted, the system assigns one automatically |
state | present (create/keep) or absent (delete) |
system | If true, create it as a system group |
state: present creates the group if it doesn’t exist and leaves it alone if it already does. Running it twice yields 0 changed on the second pass — that idempotency is the module’s default behavior.
user module: the center of user creation #
The ansible.builtin.user module is one of the most frequently used modules in RHCE. It bundles RHCSA’s useradd and usermod into a single module.
- name: Create a user
hosts: all
become: true
tasks:
- name: Create the developer account
ansible.builtin.user:
name: jdoe
uid: 5001
comment: "John Doe"
group: developers
groups: wheel
append: true
shell: /bin/bash
state: presentLet’s lay out the key options.
| Option | Meaning |
|---|---|
name | User name (required) |
uid | UID to assign |
group | Primary group |
groups | Supplementary groups. Comma-separated list or a list |
append | If true, add groups on top of the existing ones. If false (default), overwrite |
password | Encrypted password hash (not plaintext) |
shell | Login shell. e.g. /bin/bash, /sbin/nologin |
state | present or absent |
remove | With state: absent, if true, also delete the home directory |
The append trap #
If you give groups but leave out append, the default is false, so it overwrites all the existing supplementary groups and leaves only the ones you specified. On a “add the user to the wheel group” question, leaving out append: true makes the other groups disappear and costs you points. When the intent is to add supplementary groups, always write append: true alongside.
password: handle passwords as a hash #
The user module’s password takes an encrypted hash, not plaintext. If you put plaintext in directly, that very string is stored as the hash and login won’t work. You build the hash with the password_hash filter.
- name: Create a user with a password
hosts: all
become: true
vars:
user_password: "{{ vault_user_password }}"
tasks:
- name: Create jdoe and set the password
ansible.builtin.user:
name: jdoe
password: "{{ user_password | password_hash('sha512') }}"
shell: /bin/bash
state: presentHere the plaintext password vault_user_password, as covered in #10 Ansible Vault, belongs by convention in a variable file encrypted with Vault (ansible-vault create group_vars/all/vault.yml). Writing a plaintext password directly into the playbook costs you points in the exam. At run time you unlock it with --vault-password-file ~/.vault_pass.
password_hash('sha512') uses a different salt each time it’s called, so the hash changes — left as-is, every run can appear changed. If you need to satisfy idempotency strictly, there’s a way to supply a fixed salt, but on the exam what’s graded is whether the password is set correctly and login works.
Creating many users with loop: an exam favorite #
The form that shows up most often in RHCE is the pattern of taking a user list as a variable and creating them all at once with loop. We reuse the loop you learned in #9 loop. Put the user list in a variable file.
# group_vars/all/users.yml
users:
- { name: alice, groups: developers }
- { name: bob, groups: developers }
- { name: carol, groups: ops }Then create the users with a loop.
- name: Create users in bulk
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
append: true
password: "{{ default_password | password_hash('sha512') }}"
shell: /bin/bash
state: present
loop: "{{ users }}"loop: "{{ users }}" takes each element of the list as item and accesses the dictionary fields via item.name and item.groups. Even with dozens of users, you only need to extend the variable list. If you want to add a condition, you can also branch per field, like when: item.state | default('present') == 'present'. This pattern is reused as-is in the integrated playbook below.
dnf module: package management #
Package install/removal is handled by the ansible.builtin.dnf module. It corresponds to RHCSA’s dnf install and dnf remove.
- name: Manage packages
hosts: all
become: true
tasks:
- name: Install several packages
ansible.builtin.dnf:
name:
- httpd
- mariadb-server
- vim-enhanced
state: present
- name: Bring a package to its latest state
ansible.builtin.dnf:
name: tmux
state: latest
- name: Remove a package
ansible.builtin.dnf:
name: telnet
state: absentThe main options are as follows.
| Option | Meaning |
|---|---|
name | Package name. Use a list for several at once |
state | present (install), latest (update to newest), absent (remove) |
enablerepo | Enable only a specific repository for the install |
disablerepo | Disable a specific repository |
state: present installs it if absent and leaves it as is if present, so idempotency is guaranteed. state: latest, on the other hand, updates every time a new version exists, so for a “just install this package” question, using present matches the intent.
Installing package groups #
A group that bundles several packages is installed with the @ prefix. This is what you did in RHCSA with dnf group install.
- name: Install the Development Tools group
ansible.builtin.dnf:
name: "@Development Tools"
state: presentmodule stream #
An AppStream module stream is specified in the @module:stream form. As an example, let’s install nginx’s 1.22 stream.
- name: Install the nginx 1.22 module stream
ansible.builtin.dnf:
name: "@nginx:1.22"
state: present@nginx:1.22 enables the nginx module’s 1.22 stream and installs the default profile. On a question where you have to pin a specific version stream, you use this notation as is.
yum_repository module: adding a repository #
To register a repository from the internet or an internal server, you use the ansible.builtin.yum_repository module. It automates the job of hand-creating a .repo file in /etc/yum.repos.d/ in RHCSA.
- name: Register the in-house repository
hosts: all
become: true
tasks:
- name: Add the BaseOS repo
ansible.builtin.yum_repository:
name: internal-baseos
description: "Internal BaseOS Repository"
baseurl: http://repo.example.com/baseos
gpgcheck: true
gpgkey: http://repo.example.com/RPM-GPG-KEY-internal
enabled: true
state: presentLet’s lay out the main options.
| Option | Meaning |
|---|---|
name | Repo ID. Becomes the section name inside the .repo file |
description | Human-readable description. The name= entry in the .repo |
baseurl | Base URL from which packages are fetched |
gpgcheck | If true, verify GPG signatures |
gpgkey | Location of the GPG key used for verification |
enabled | Whether this repo is enabled |
file | Name of the .repo file to save to (uses name if omitted) |
name becomes the section ID of the .repo file, and description goes in as the name= entry inside it. The two fields are easy to mix up, so memorize the pairing from the table. With gpgcheck: true, you also have to specify gpgkey for verification to pass at install time.
An exam favorite: the integrated playbook #
In the actual exam, the modules above come together in a single playbook. The form “add a repository, install packages from that repo, and create a user list with loop” is typical. Let’s bundle it into one file.
- name: RHCSA automation integrated
hosts: webservers
become: true
vars_files:
- group_vars/all/users.yml
- group_vars/all/vault.yml
tasks:
- name: Register the in-house repository
ansible.builtin.yum_repository:
name: internal-appstream
description: "Internal AppStream"
baseurl: http://repo.example.com/appstream
gpgcheck: false
enabled: true
- name: Install web packages
ansible.builtin.dnf:
name:
- httpd
- "@nginx:1.22"
state: present
enablerepo: internal-appstream
- name: Create the ops group
ansible.builtin.group:
name: ops
state: present
- name: Create users in bulk
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
append: true
password: "{{ vault_default_password | password_hash('sha512') }}"
shell: /bin/bash
state: present
loop: "{{ users }}"This one playbook contains all of this post’s modules. The order is natural: register the repository, install packages from that repo with enablerepo, create the group first, then create the users with loop.
Exam points #
passwordonly takes a hash. Put plaintext in and login won’t work. Build the hash with thepassword_hash('sha512')filter, and keep the plaintext password in a Vault-encrypted variable.- Don’t forget
append: true. On a question asking you to add supplementary groups, leaving outappendmakes all the existing supplementary groups disappear. When the intent is to add, always write it alongside. - Distinguish
yum_repository’snamefromdescription.nameis the repo ID,descriptionis the.repo’sname=description. Mix up the pairing and verification goes wrong. - Module stream uses the
@module:streamnotation. As an example, write it like@nginx:1.22. - Distinguish
state: presentfromlatest. “Just install” meanspresent, “update to the latest” meanslatest. - Create the group before the user. If the user’s supplementary group doesn’t exist, the task fails. Within one playbook, put the group task above the user task.
Wrap-up #
What we automated in this post:
- group module. Create a group idempotently with
name,gid,state - user module.
name,uid,group,groups,append,shell,state. The password ispassword_hash+ Vault - dnf module.
name,state(present/latest/absent),enablerepo. Package groups use@group, module streams use@module:stream - yum_repository module. Register a repo with
name,description,baseurl,gpgcheck,gpgkey,enabled - loop pattern. The exam favorite of creating users in bulk from a user-list variable with loop
- integrated playbook. Register repo → install packages → create groups and users in one file
Next: RHCSA Automation 2 #
We moved users/groups and packages/repositories into playbooks. Now it’s time to automate the services that run on top of them.
In #15 RHCSA Automation 2: services, chronyd, logs, we’ll cover how to start and enable services with the service and systemd modules, how to configure chronyd time synchronization as plays, and log-related settings too, turning manual work into modules.