Red Hat Certified Engineer (RHCE) #15 RHCSA Automation 2: Services, chronyd, log

4 min read

In #14 RHCSA Automation 1 we locked down users/groups and packages/repositories with playbooks. This time, in #15, we automate the second cluster of RHCSA manual work — service management, time synchronization, and log configuration. These are all tasks we handled by hand with systemctl, chronyc, and journalctl in RHCSA #9 Services and booting, and this time we’ll declare the same outcomes with idempotent modules.

The grading point in this area is always the same. A service isn’t done once it’s running (started) — it also has to come up at boot (enabled). Time synchronization requires restarting the daemon after the config file changes, and logs have to be configured to persist across reboots. In other words, the heart of automation is guaranteeing both “works now” and “still works after a reboot” at once.

Service management: the service and systemd modules #

Starting, stopping, and registering a daemon at boot is done with the ansible.builtin.service or ansible.builtin.systemd_service module. You merge the systemctl start and systemctl enable you used to type by hand into a single task.

The three core keys of the service module #

KeyMeaningExample values
nameThe target service namehttpd, chronyd
stateThe current running statestarted, stopped, restarted, reloaded
enabledWhether to auto-start at boottrue, false

The part most often missed here is specifying state and enabled together. Writing only state: started turns it on now but it may be off after a reboot; writing only enabled: true registers it for boot but it’s stopped right now. The exam almost always requires both, so we’ll build the habit of writing both keys together.

Playbook to enable and start httpd
- name: 웹 서버를 지금도 켜고 부팅에도 등록한다
  hosts: webservers
  become: true
  tasks:
    - name: httpd 패키지 설치
      ansible.builtin.dnf:
        name: httpd
        state: present

    - name: httpd 서비스 enable + start
      ansible.builtin.service:
        name: httpd
        state: started
        enabled: true

The difference between service and systemd_service #

The service module is a general-purpose module that auto-detects the init system, while the systemd_service module is systemd-only and additionally provides systemd-specific features like daemon_reload and masked. RHEL 9 uses systemd, so both work, but when you’ve placed a unit file directly and need it re-read, the systemd module has the edge.

daemon_reload after unit changes
- name: 사용자 정의 unit 배치 후 데몬 리로드
  ansible.builtin.systemd_service:
    name: myapp
    state: started
    enabled: true
    daemon_reload: true

daemon_reload: true corresponds to systemctl daemon-reload and makes systemd recognize the change right after you place or edit a unit file in /etc/systemd/system.

Time synchronization: the timesync system role and a chrony template #

Time synchronization is a perennial RHCE topic. RHEL 9’s NTP implementation is chrony, and the daemon name is chronyd. There are two routes to automating it: using the timesync system role, and deploying chrony.conf directly as a template.

Method 1: the timesync system role #

Using rhel-system-roles, covered in #13 system roles, you pass only the NTP server list as a variable and leave the rest to the role. This is the shortest and safest route.

timesync role playbook
- name: timesync system role로 NTP 구성
  hosts: all
  become: true
  vars:
    timesync_ntp_servers:
      - hostname: 0.kr.pool.ntp.org
        iburst: true
      - hostname: 1.kr.pool.ntp.org
        iburst: true
  roles:
    - redhat.rhel_system_roles.timesync

The role handles chrony installation, writing the config file, and enabling and starting the service all in one go, so it’s the fastest response to an exam task that says “configure time synchronization to use these NTP servers.”

Method 2: chrony.conf template + handler #

You can also handle the config file directly without a system role. The structure deploys chrony.conf with the template module and restarts chronyd via a handler only when the file changes. This pattern is the standard form for every “restart the service after changing a config file” task, so be sure to master it.

templates/chrony.conf.j2:

templates/chrony.conf.j2
# Ansible managed
{% for server in chrony_servers %}
server {{ server }} iburst
{% endfor %}

driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony

The playbook:

Playbook to deploy chrony.conf
- name: chrony.conf 템플릿으로 시간 동기화 구성
  hosts: all
  become: true
  vars:
    chrony_servers:
      - 0.kr.pool.ntp.org
      - 1.kr.pool.ntp.org
  tasks:
    - name: chrony 패키지 설치
      ansible.builtin.dnf:
        name: chrony
        state: present

    - name: chrony.conf 배포
      ansible.builtin.template:
        src: chrony.conf.j2
        dest: /etc/chrony.conf
        owner: root
        group: root
        mode: '0644'
      notify: restart chronyd

    - name: chronyd enable + start
      ansible.builtin.service:
        name: chronyd
        state: started
        enabled: true

  handlers:
    - name: restart chronyd
      ansible.builtin.service:
        name: chronyd
        state: restarted

The flow matters here. When the template task changes the file, notify schedules the handler, and after all tasks in the playbook finish, the handler runs once to restart chronyd. If the file doesn’t change, the handler isn’t called, so on the second run no restart happens and idempotency holds.

Job scheduling: the cron and at modules #

Recurring jobs are managed with the ansible.builtin.cron module. Instead of editing crontab entries directly, the module adds and removes entries idempotently.

The main keys of the cron module #

KeyMeaning
nameThe entry’s identifying name. The basis for idempotency
jobThe command to run
minute / hour / day / month / weekdayThe run time. The default is *
userWhose crontab
statepresent or absent

The key point is that name is the basis for idempotency. The cron module also records an #Ansible: <name> comment in the crontab, and on the next run, when it meets the same name, it updates the existing entry instead of adding a new one. Changing the name every time causes duplicate entries to pile up — keep the name consistent.

Register a cron job
- name: 정기 백업 cron 작업 등록
  hosts: dbservers
  become: true
  tasks:
    - name: 매일 02:30 백업 스크립트 실행
      ansible.builtin.cron:
        name: nightly-backup
        user: root
        minute: "30"
        hour: "2"
        job: "/usr/local/bin/backup.sh"
        state: present

The task above registers the 30 2 * * * /usr/local/bin/backup.sh entry in root’s crontab idempotently. The unspecified day, month, and weekday automatically become *.

If you need to drop a file into a specific directory, you can also create a file under /etc/cron.d with cron_file. In that case it’s the system-wide crontab format, so you specify user as well.

Register via cron_file in /etc/cron.d
- name: /etc/cron.d 파일로 등록
  ansible.builtin.cron:
    name: log-rotate-check
    user: root
    minute: "0"
    hour: "1"
    job: "/usr/local/bin/check-logs.sh"
    cron_file: custom-log-check

One-off jobs are scheduled with the ansible.builtin.at module. For example, giving ansible.builtin.at a command plus count and units: minutes registers an at job that runs exactly once after the specified time.

Automating journald persistent storage #

On stock RHEL, journald logs are stored in the memory (volatile) area of /run/log/journal and disappear on reboot. To preserve logs persistently you have to change Storage=persistent in /etc/systemd/journald.conf and restart the daemon. This too is automated with the template and handler pattern.

templates/journald.conf.j2:

templates/journald.conf.j2
# Ansible managed
[Journal]
Storage=persistent
SystemMaxUse={{ journald_max_use | default('500M') }}

The playbook:

Playbook for persistent journald
- name: journald 로그 영구 저장 설정
  hosts: all
  become: true
  vars:
    journald_max_use: 1G
  tasks:
    - name: 영구 저장 디렉터리 생성
      ansible.builtin.file:
        path: /var/log/journal
        state: directory
        owner: root
        group: systemd-journal
        mode: '2755'

    - name: journald.conf 배포
      ansible.builtin.template:
        src: journald.conf.j2
        dest: /etc/systemd/journald.conf
        owner: root
        group: root
        mode: '0644'
      notify: restart journald

  handlers:
    - name: restart journald
      ansible.builtin.systemd_service:
        name: systemd-journald
        state: restarted

The /var/log/journal directory has to exist for journald to write persistent logs there, so we create it first with the file module. When the config file changes, the handler performs a restart corresponding to systemctl restart systemd-journald.

Applying a tuned profile #

tuned, which manages performance profiles, is also an automation target. Instead of typing tuned-adm by hand, you apply a profile with the tuned system role or the command module. The system role is the cleanest.

tuned role playbook
- name: tuned 프로파일 적용
  hosts: dbservers
  become: true
  vars:
    tuned_profile: throughput-performance
  roles:
    - redhat.rhel_system_roles.tuned

If you don’t use the system role, bring up the tuned service with enable and start, then apply tuned-adm profile <name> with the command module — but refine the changed_when condition so it doesn’t report changed when the profile is already applied.

Exam points #

  • enabled and state are a pair. Service tasks almost always require both “turn it on now (state: started) and register it for boot (enabled: true)” at once. Writing only one satisfies half the grading.
  • Restart config changes with a handler. After changing a config file like chrony.conf or journald.conf with template, always restart the daemon via notify and a handler. Calling restart directly in a task every time restarts even when the file didn’t change, breaking idempotency.
  • NTP is faster with the system role. For time synchronization tasks, the safest move is passing just the server list to the timesync system role. Master the chrony.conf template approach too, in case you can’t use the role.
  • cron is idempotent by name. The cron module’s name is the entry identifier, so keep the same name for the same job to prevent duplicate registration.
  • journald persistence starts with the directory. Creating /var/log/journal, setting Storage=persistent, and restarting systemd-journald are one bundle.
  • Run it twice and confirm changed=0. Every playbook should report changed=0 on the second run. Check whether a handler fires every time, or whether a command/shell task is breaking idempotency.

Wrap-up #

What this post locked in:

  • service / systemd_service modules. Declare a daemon through enable and start in one shot with name/state/enabled. daemon_reload on unit changes
  • Time synchronization. The timesync system role (just the server list as a variable) or chrony.conf template + handler restart
  • cron / at modules. Idempotency by cron’s name; register recurring jobs with minute/hour/user/job/state. One-off scheduling with at
  • journald persistent storage. Creating /var/log/journal + journald.conf template + a systemd-journald restart handler
  • tuned. Apply a performance profile with the system role or command
  • The common principle. Restart config changes with a handler, give services both enabled and state, and verify idempotency by running twice

Next: RHCSA Automation 3 #

We’ve automated services, time, and logs. Next is the area RHCSA demanded the most hands-on work — storage.

In #16 RHCSA Automation 3: storage (LVM), filesystems (NFS), we’ll create volume groups and logical volumes with the lvg and lvol modules, create filesystems and mount them permanently with the filesystem and mount modules, declare it all at once with the storage system role, and cover NFS client mounts with playbooks too.

X