Red Hat Certified Engineer (RHCE) #14 RHCSA Automation 1: Users/Groups, Packages/Repositories

9 min read

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.

Create the developers group
- name: Ensure the developers group
  ansible.builtin.group:
    name: developers
    gid: 5000
    state: present

The main options are simple.

OptionMeaning
nameGroup name (required)
gidGID to assign. If omitted, the system assigns one automatically
statepresent (create/keep) or absent (delete)
systemIf 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.

Create a user with the user 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: present

Let’s lay out the key options.

OptionMeaning
nameUser name (required)
uidUID to assign
groupPrimary group
groupsSupplementary groups. Comma-separated list or a list
appendIf true, add groups on top of the existing ones. If false (default), overwrite
passwordEncrypted password hash (not plaintext)
shellLogin shell. e.g. /bin/bash, /sbin/nologin
statepresent or absent
removeWith 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.

Set the password with password_hash
- 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: present

Here 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
# 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.

Create users in bulk with 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.

Manage packages with dnf
- 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: absent

The main options are as follows.

OptionMeaning
namePackage name. Use a list for several at once
statepresent (install), latest (update to newest), absent (remove)
enablerepoEnable only a specific repository for the install
disablerepoDisable 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.

Install a package group
- name: Install the Development Tools group
  ansible.builtin.dnf:
    name: "@Development Tools"
    state: present

module stream #

An AppStream module stream is specified in the @module:stream form. As an example, let’s install nginx’s 1.22 stream.

Install a module 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.

Register a repo with yum_repository
- 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: present

Let’s lay out the main options.

OptionMeaning
nameRepo ID. Becomes the section name inside the .repo file
descriptionHuman-readable description. The name= entry in the .repo
baseurlBase URL from which packages are fetched
gpgcheckIf true, verify GPG signatures
gpgkeyLocation of the GPG key used for verification
enabledWhether this repo is enabled
fileName 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.

Combined playbook
- 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 #

  • password only takes a hash. Put plaintext in and login won’t work. Build the hash with the password_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 out append makes all the existing supplementary groups disappear. When the intent is to add, always write it alongside.
  • Distinguish yum_repository’s name from description. name is the repo ID, description is the .repo’s name= description. Mix up the pairing and verification goes wrong.
  • Module stream uses the @module:stream notation. As an example, write it like @nginx:1.22.
  • Distinguish state: present from latest. “Just install” means present, “update to the latest” means latest.
  • 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 is password_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.

X