Red Hat Certified Engineer (RHCE) #15 RHCSA Automation 2: Services, chronyd, log
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 #
| Key | Meaning | Example values |
|---|---|---|
| name | The target service name | httpd, chronyd |
| state | The current running state | started, stopped, restarted, reloaded |
| enabled | Whether to auto-start at boot | true, 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.
- 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: trueThe 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.
- name: 사용자 정의 unit 배치 후 데몬 리로드
ansible.builtin.systemd_service:
name: myapp
state: started
enabled: true
daemon_reload: truedaemon_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.
- 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.timesyncThe 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:
# Ansible managed
{% for server in chrony_servers %}
server {{ server }} iburst
{% endfor %}
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chronyThe playbook:
- 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: restartedThe 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 #
| Key | Meaning |
|---|---|
| name | The entry’s identifying name. The basis for idempotency |
| job | The command to run |
| minute / hour / day / month / weekday | The run time. The default is * |
| user | Whose crontab |
| state | present 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.
- 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: presentThe 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.
- 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-checkOne-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:
# Ansible managed
[Journal]
Storage=persistent
SystemMaxUse={{ journald_max_use | default('500M') }}The playbook:
- 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: restartedThe /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.
- name: tuned 프로파일 적용
hosts: dbservers
become: true
vars:
tuned_profile: throughput-performance
roles:
- redhat.rhel_system_roles.tunedIf 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
notifyand 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, settingStorage=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.