Managing Services with Systemd in Ansible
Objective
This guide covers service management in Ansible using the systemd module. By the end, you will know how to:
- Start, stop, restart, and reload services on remote hosts using Ansible
- Enable and disable services so they start automatically at boot
- Use
daemon_reloadto pick up new or modified unit files before managing services - Combine service management with package installation and configuration deployment in real playbooks
- Understand the difference between the
systemdandservicemodules and when to use each - Apply these patterns to RHCE exam tasks involving service automation
Managing services manually on one server is straightforward. Managing them across fifty servers consistently is not, unless you automate it. Ansible's systemd module lets you define the desired state of any service and apply it across your entire inventory in a single playbook run, with the same result every time regardless of the current state of each host.
systemd Module vs service Module
Ansible has two modules for service management. Understanding the difference helps you choose the right one:
| Module | What It Does | When to Use It | systemd-specific Features |
|---|---|---|---|
ansible.builtin.service |
Generic service management, works with SysV init, Upstart, and systemd | When you need portability across different init systems | No: cannot use daemon_reload, scope, or unit |
ansible.builtin.systemd |
systemd-specific service management | On RHEL, Fedora, Ubuntu 16.04+, and any modern Linux using systemd | Yes: daemon_reload, masked, scope, unit file path |
For RHCE exam tasks on RHEL, always use ansible.builtin.systemd. It gives you full control over systemd-specific behaviour that the generic service module cannot access.
The systemd Module: Key Parameters
| Parameter | Values | What It Controls |
|---|---|---|
name |
Service name (e.g. nginx, sshd) |
Which service to manage |
state |
started, stopped, restarted, reloaded |
The desired runtime state of the service |
enabled |
true, false |
Whether the service starts automatically at boot |
daemon_reload |
true, false |
Runs systemctl daemon-reload before managing the service |
masked |
true, false |
Masks or unmasks the service (prevents it from being started by any means) |
force |
true, false |
Forces the operation even if the service is in an unexpected state |
Basic Service Management
Starting and Enabling a Service
The most common task: ensure a service is running right now and configured to start automatically after a reboot.
---
- name: Manage nginx service
hosts: webservers
tasks:
- name: Start nginx and enable it at boot
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
What Ansible does with this task:
- If nginx is already running and enabled: does nothing, reports
ok - If nginx is stopped: starts it, reports
changed - If nginx is not enabled: enables it, reports
changed - Both
stateandenabledare checked independently. You can start without enabling, or enable without starting, or do both at once.
Stopping and Disabling a Service
---
- name: Disable and stop a service
hosts: all
tasks:
- name: Stop and disable firewalld
ansible.builtin.systemd:
name: firewalld
state: stopped
enabled: false
Restarting a Service
---
- name: Restart a service
hosts: webservers
tasks:
- name: Restart nginx to apply new configuration
ansible.builtin.systemd:
name: nginx
state: restarted
Note: restarted always restarts the service even if it was already running. Use this carefully in production. For most configuration changes, triggering a restart through a handler (shown below) is safer because it only restarts when something actually changed.
Reloading a Service
- name: Reload nginx configuration without downtime
ansible.builtin.systemd:
name: nginx
state: reloaded
The difference between restarted and reloaded: a restart stops and starts the process (brief interruption), a reload sends a signal to the running process to re-read its configuration without stopping (no interruption). Not every service supports reload. Check your service documentation before relying on it.
daemon_reload: When and Why
When you create a new systemd unit file or modify an existing one, systemd does not know about the change until you run systemctl daemon-reload. Ansible's daemon_reload: true parameter handles this automatically before performing the service operation.
---
- name: Deploy a custom service unit and start it
hosts: all
tasks:
- name: Copy the custom service unit file
ansible.builtin.copy:
src: files/myapp.service
dest: /etc/systemd/system/myapp.service
owner: root
group: root
mode: '0644'
- name: Reload systemd and start the new service
ansible.builtin.systemd:
name: myapp
state: started
enabled: true
daemon_reload: true
When daemon_reload: true is set, Ansible runs the equivalent of systemctl daemon-reload before touching the service. Without this, starting a newly copied unit file would fail because systemd still has no record of it.
The Right Pattern: Handlers for Service Restarts
The most common mistake in Ansible service management is using state: restarted directly in a task. This restarts the service every time the playbook runs, even when nothing changed. The correct pattern is to use a handler that only triggers when a configuration file or unit file actually changes.
---
- name: Install and configure nginx
hosts: webservers
tasks:
- name: Install nginx
ansible.builtin.dnf:
name: nginx
state: present
- name: Deploy nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: nginx -t -c %s
notify: Restart nginx
- name: Deploy virtual host configuration
ansible.builtin.template:
src: vhost.conf.j2
dest: /etc/nginx/conf.d/mysite.conf
owner: root
group: root
mode: '0644'
notify: Restart nginx
- name: Start and enable nginx
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
handlers:
- name: Restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
How handlers work here:
- The
notify: Restart nginxdirective queues the handler when the task reportschanged - If neither configuration file changed, neither task reports changed, and the handler never runs
- If both configuration files changed, the handler is queued twice but runs only once at the end of the play
- The service restarts exactly when it needs to, and never when it doesn't
A Complete Real-World Example: Installing and Managing a Web Stack
---
- name: Deploy and manage LAMP stack services
hosts: webservers
vars:
http_port: 80
db_service: mariadb
tasks:
- name: Install required packages
ansible.builtin.dnf:
name:
- httpd
- mariadb-server
- php
- php-mysqlnd
state: present
- name: Deploy Apache configuration
ansible.builtin.template:
src: httpd.conf.j2
dest: /etc/httpd/conf/httpd.conf
owner: root
group: root
mode: '0644'
validate: httpd -t -f %s
notify: Restart httpd
- name: Start and enable Apache
ansible.builtin.systemd:
name: httpd
state: started
enabled: true
- name: Start and enable MariaDB
ansible.builtin.systemd:
name: "{{ db_service }}"
state: started
enabled: true
- name: Ensure firewalld is running
ansible.builtin.systemd:
name: firewalld
state: started
enabled: true
- name: Open HTTP port in firewalld
ansible.posix.firewalld:
service: http
permanent: true
state: enabled
immediate: true
handlers:
- name: Restart httpd
ansible.builtin.systemd:
name: httpd
state: restarted
Managing Multiple Services at Once
---
- name: Ensure all required services are running
hosts: all
vars:
required_services:
- name: sshd
enabled: true
state: started
- name: chronyd
enabled: true
state: started
- name: rsyslog
enabled: true
state: started
- name: auditd
enabled: true
state: started
tasks:
- name: Manage all required services
ansible.builtin.systemd:
name: "{{ item.name }}"
state: "{{ item.state }}"
enabled: "{{ item.enabled }}"
loop: "{{ required_services }}"
Looping over a list of services with their desired state keeps the playbook clean and makes it easy to add or remove services by editing the variable, not the task logic.
Masking Services
Masking a service prevents it from being started by any means, including manual systemctl start commands, dependencies, or other services. It is stronger than simply disabling.
---
- name: Harden system by masking unnecessary services
hosts: all
tasks:
- name: Mask the bluetooth service
ansible.builtin.systemd:
name: bluetooth
masked: true
- name: Mask the avahi-daemon service
ansible.builtin.systemd:
name: avahi-daemon
masked: true
- name: Unmask a service that was previously masked
ansible.builtin.systemd:
name: cups
masked: false
enabled: false
state: stopped
Verifying Service State in a Playbook
After managing a service, you can verify its state programmatically using the service_facts module and then act on the result:
---
- name: Verify service state after management
hosts: all
tasks:
- name: Start nginx
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
- name: Gather service facts
ansible.builtin.service_facts:
- name: Confirm nginx is running
ansible.builtin.debug:
msg: "nginx is {{ ansible_facts.services['nginx.service'].state }}"
when: "'nginx.service' in ansible_facts.services"
- name: Fail if nginx is not running
ansible.builtin.fail:
msg: "nginx failed to start on {{ inventory_hostname }}"
when:
- "'nginx.service' in ansible_facts.services"
- ansible_facts.services['nginx.service'].state != 'running'
Common Mistakes
- Using state: restarted directly instead of a handler. A task with
state: restartedrestarts the service on every playbook run regardless of whether anything changed. This causes unnecessary downtime and is incorrect for idempotent automation.- Use
state: startedin the service task and trigger restarts through handlers notified by configuration changes
- Use
- Forgetting daemon_reload after deploying a unit file. If you copy a new
.servicefile to/etc/systemd/system/and immediately try to start the service withoutdaemon_reload: true, systemd will not find the unit and the task will fail.- Always set
daemon_reload: trueon the firstsystemdtask that runs after writing a unit file
- Always set
- Confusing enabled with state.
enabled: truemakes the service start at boot but does not start it right now.state: startedstarts it now but does not affect boot behaviour. Most tasks need both.- For the RHCE exam specifically: a service that is started but not enabled fails the reboot check
- Using the service module instead of systemd on RHEL. The generic
servicemodule works but cannot usedaemon_reloadormasked. On RHEL systems where these features matter, always useansible.builtin.systemd. - Not validating configuration before restarting. Restarting a service with a broken configuration file takes it down with no automatic recovery. Use the
validateoption on the template or copy task before the service restart handler runs.
State Values: What Each One Does
| State Value | Equivalent systemctl Command | Behaviour | Idempotent? |
|---|---|---|---|
started |
systemctl start |
Starts the service if not already running. Does nothing if already running. | Yes |
stopped |
systemctl stop |
Stops the service if running. Does nothing if already stopped. | Yes |
restarted |
systemctl restart |
Always stops and starts the service, regardless of current state. | No |
reloaded |
systemctl reload |
Sends SIGHUP to the process to re-read configuration without stopping. | No |
Quick Reference
# Start a service and enable it at boot
- ansible.builtin.systemd:
name: nginx
state: started
enabled: true
# Stop a service and disable it
- ansible.builtin.systemd:
name: bluetooth
state: stopped
enabled: false
# Reload systemd after writing a unit file, then start the service
- ansible.builtin.systemd:
name: myapp
state: started
enabled: true
daemon_reload: true
# Mask a service (prevent it from starting by any means)
- ansible.builtin.systemd:
name: avahi-daemon
masked: true
# Restart via handler (correct pattern for config changes)
handlers:
- name: Restart nginx
ansible.builtin.systemd:
name: nginx
state: restarted
Service Management and the RHCE Exam
Service management with the systemd module is a core RHCE exam objective. Exam tasks in this area commonly look like:
- Install a package and ensure its service is started and enabled across all managed hosts
- Deploy a configuration file and restart the service only if the file changed
- Ensure a list of services are all running and enabled on specific host groups
- Deploy a custom systemd unit file and start the service it defines
- Disable and stop a service that should not be running in a hardened environment
The exam environment reboots managed hosts after time ends and checks whether services are in the expected state. A service started with state: started but not enabled: true will be down after the reboot and the task scores zero. Both parameters are always required when the task says the service should be running.
Practice This in a Real Environment
Service management looks simple on paper. The subtleties, particularly the difference between start and enable, the handler pattern for restarts, and when to use daemon_reload, only become clear when you have watched an idempotent playbook run multiple times and seen exactly what changes and what doesn't.
LinuxCert.Guru has a dedicated hands-on lab for this: Managing Services with Systemd. It covers starting, stopping, enabling, reloading, and automating service management on real RHEL hosts with auto-graded tasks that verify both the running state and the boot enablement, the same way the RHCE exam checks your work.
Practice the Managing Services with Systemd lab at LinuxCert.Guru → https://linuxcert.guru/rhce/?name=rhce-managing-services-systemd
Conclusion
Ansible's systemd module gives you complete control over service state across your entire infrastructure from a single playbook. The concepts are straightforward but the patterns matter for both reliability and exam performance.
- Use
ansible.builtin.systemdon RHEL, not the genericservicemodule, for full systemd feature access - Always set both
stateandenabledwhen a service needs to be running after reboots - Use handlers with
state: restartedrather than putting restarts directly in tasks - Set
daemon_reload: truewhenever you deploy a new or modified unit file before managing the service - Use
masked: truewhen a service should be completely prevented from starting, not just disabled - Validate configuration files before the service restart handler runs to avoid taking services down with broken configs