Terraform and NREC: Part IV - Pairing with Ansible¶
Last changed: 2024-09-17
This document builds on Terraform Part 1, Part 2 and Part 3. In Part 3 we built an environment in NREC consisting of 4 frontend web servers and 1 backend database server, as well as security groups and an SSH key pair for access. However, we didn’t do anything inside the operating systems of our instances to make them act as web servers or database servers. Terraform only interacts with the Openstack API, and doesn’t do anything inside the instances. Here is where Ansible comes into play. Using Ansible, we’ll configure the web servers and the database server to make a real service.
This is not an introduction to Ansible. It is assumed that the reader has some knowledge and experience in using Ansible.
The files used in this document can be downloaded:
The examples in this document have been tested and verified with Terraform version 1.9.5:
Terraform v1.9.5
on linux_amd64
+ provider registry.terraform.io/ansible/ansible v1.3.0
+ provider registry.terraform.io/terraform-provider-openstack/openstack v2.1.0
Installing Ansible¶
Ansible is available in most Linux distributions. If possible, use the Ansible version availble from the distribution. For Fedora, and for RHEL or equivalent with EPEL enabled, simply install using yum:
# yum install ansible
For Debian and Ubuntu:
# apt-get install ansible
When using Ansible with NREC and instances created with Terraform,
we’ll often create and destroy instances multiple times. This depends
on your workflow. It may be beneficial to add the following
configuration to your ~/.ansible.cfg
to prevent Ansible from
halting on unknown SSH host keys:
[defaults]
host_key_checking = False
Installing Terraform Plugin for Ansible¶
Ansible has a Terraform collection that reads information from the
Terraform state file terraform.tfstate
. This needs to be installed
for this to work. Run the following command to install it:
ansible-galaxy collection install cloud.terraform
On Linux, this installs the Terraform collection under:
~/.ansible/collections/ansible_collections
You can verify the Terraform collection like this:
$ ansible-galaxy collection list | grep terraform
cloud.terraform 3.0.0
Ansible Information in Terraform State¶
The Terraform collection for Ansible expects certain data in the Terraform
state. We will provide this by adding some extra statements to our
Terraform main.tf
file. First we need to add the Ansible plugin:
1# Define required providers
2terraform {
3 required_version = ">= 1.0"
4 required_providers {
5 openstack = {
6 source = "terraform-provider-openstack/openstack"
7 }
8 ansible = {
9 version = "~> 1.3.0"
10 source = "ansible/ansible"
11 }
12 }
13}
Running terraform init will then add the Ansible plugin along with the Openstack plugin.
Next, we need to add statements that tells Ansible the relevant details of our hosts. We define our web and database hosts, as well as groups that contain these hosts:
1# Ansible web hosts
2resource "ansible_host" "web" {
3 count = lookup(var.role_count, "web", 0)
4 name = "${var.region}-web-${count.index}"
5 groups = ["osl_web"] # Groups this host is part of
6
7 variables = {
8 ansible_host = trim(openstack_compute_instance_v2.web_instance[count.index].access_ip_v6, "[]")
9 }
10}
11
12# Ansible db hosts
13resource "ansible_host" "db" {
14 count = lookup(var.role_count, "db", 0)
15 name = "${var.region}-db-${count.index}"
16 groups = ["osl_db"] # Groups this host is part of
17
18 variables = {
19 ansible_host = trim(openstack_compute_instance_v2.db_instance[count.index].access_ip_v6, "[]")
20 }
21}
22
23# Ansible web group
24resource "ansible_group" "web_group" {
25 name = "web"
26 children = ["osl_web"]
27 variables = {
28 ansible_user = "almalinux"
29 }
30}
31
32# Ansible db group
33resource "ansible_group" "db_group" {
34 name = "db"
35 children = ["osl_db"]
36 variables = {
37 ansible_user = "ubuntu"
38 }
39}
Note the use of the trim()
function for the ansible_host
variable. This is needed when using the IPv6 address.
Ansible Inventory File¶
The whole point is that the inventory should be generated dynamically based on the Terraform state. But we need to tell Ansible where to locate what it needs. For this we create an inventory file that we call “terraform.yaml”:
1plugin: cloud.terraform.terraform_provider
2project_path: /path/to/project
3# Terraform binary (available in the $PATH) or full path to the binary.
4binary_path: terraform
You will need to edit this file and set the full path to the Terraform
directory, i.e. where you terraform.tfstate
is located.
Testing Ansible¶
Note
When using the cloud.terraform
collection with an unsupported
version of Ansible, we get a warning as seen in the examples
below. It still works in our case (RHEL9).
With the terraform.yaml
file in place, we can run ansible to test
and verify that it is able to reach the instances over the network:
First, we’ll see that our inventory is correct:
$ ansible-inventory -i terraform.yaml --graph --vars
[WARNING]: Collection cloud.terraform does not support Ansible version 2.14.14
@all:
|--@ungrouped:
|--@db:
| |--@osl_db:
| | |--osl-db-0
| | | |--{ansible_host = 2001:700:2:8201::10cf}
| | | |--{ansible_user = ubuntu}
| |--{ansible_user = ubuntu}
|--@web:
| |--@osl_web:
| | |--osl-web-0
| | | |--{ansible_host = 2001:700:2:8201::11ae}
| | | |--{ansible_user = almalinux}
| | |--osl-web-1
| | | |--{ansible_host = 2001:700:2:8201::1414}
| | | |--{ansible_user = almalinux}
| | |--osl-web-2
| | | |--{ansible_host = 2001:700:2:8201::1236}
| | | |--{ansible_user = almalinux}
| | |--osl-web-3
| | | |--{ansible_host = 2001:700:2:8201::111d}
| | | |--{ansible_user = almalinux}
| |--{ansible_user = almalinux}
Next, we’ll ping the instances using Ansible:
$ ansible -i terraform.yaml all -m ping
[WARNING]: Collection cloud.terraform does not support Ansible version 2.14.14
osl-web-2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
osl-web-1 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
osl-web-3 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
osl-web-0 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
osl-db-0 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}
We can also run a command on the instances to verify that SSH connection is working:
$ ansible -i terraform.yaml all -m shell -a 'uname -sr'
[WARNING]: Collection cloud.terraform does not support Ansible version 2.14.14
osl-db-0 | CHANGED | rc=0 >>
Linux 6.8.0-41-generic
osl-web-0 | CHANGED | rc=0 >>
Linux 5.14.0-427.31.1.el9_4.x86_64
osl-web-2 | CHANGED | rc=0 >>
Linux 5.14.0-427.31.1.el9_4.x86_64
osl-web-1 | CHANGED | rc=0 >>
Linux 5.14.0-427.31.1.el9_4.x86_64
osl-web-3 | CHANGED | rc=0 >>
Linux 5.14.0-427.31.1.el9_4.x86_64
We have verified that Ansible and dynamic inventory from Terraform state works, and are ready to proceed.
Using Ansible¶
Important
This section includes simple playbooks to show how Ansible can be used for configuring the OS and services. In order to make this into a real service for production use, a lot more work needs to be done.
I order to configure the web and database servers, we have created two
playbooks. They are named web.yaml
and db.yaml
,
respectively. We’ll take a look at web.yaml
first:
1---
2- hosts: web
3 become: true
4
5 tasks:
6 - name: Install Apache
7 ansible.builtin.yum:
8 name: httpd
9
10 - name: Install php
11 ansible.builtin.yum:
12 name: php
13
14 - name: Install php-mysqlnd
15 ansible.builtin.yum:
16 name: php-mysqlnd
17
18 - name: Set httpd_can_network_connect_db SELinux boolean
19 ansible.builtin.seboolean:
20 name: httpd_can_network_connect_db
21 state: yes
22 persistent: yes
23
24 - name: Make sure Apache is running and enabled
25 ansible.builtin.systemd:
26 name: httpd
27 state: started
28 enabled: yes
In this playbook, we do the following:
Install the Apache web server, PHP and the PHP MySQL bindings
Make sure that SELinux allows Apache to connect to the database
Make sure that the web service is enabled and running.
Next, lets take a look at db.yaml
which we use to configure the
database server:
1---
2- hosts: db
3 become: true
4
5 tasks:
6 - name: Create XFS filesystem on /dev/sdb
7 ansible.builtin.filesystem:
8 fstype: xfs
9 dev: /dev/sdb
10
11 - name: Mount volume
12 ansible.builtin.mount:
13 path: /var/lib/mysql
14 src: /dev/sdb
15 fstype: xfs
16 state: mounted
17
18 - name: Install MariaDB, also starts the service
19 ansible.builtin.apt:
20 name: mariadb-server
21 update_cache: yes
22
23 - name: Set MariaDB bind-address
24 ansible.builtin.ini_file:
25 path: /etc/mysql/mariadb.conf.d/90-server.cnf
26 section: mysqld
27 option: bind-address
28 value: "{{ ansible_default_ipv4.address }}"
29 backup: yes
30 notify:
31 - restart MariaDB
32
33 - name: Make sure MariaDB is running and enabled
34 ansible.builtin.systemd:
35 name: mariadb
36 state: started
37 enabled: yes
38
39 - name: Install PyMySQL, needed by Ansible MySQL module
40 ansible.builtin.apt:
41 name: python3-pymysql
42
43
44 handlers:
45 - name: restart MariaDB
46 ansible.builtin.systemd:
47 name: "mariadb"
48 state: restarted
In this playbook, we do the following:
Create a filesystem on our volume, available as the
/dev/sdb
device, and mount it as/var/lib/mysql
Install MariaDB (i.e. MySQL)
Set the MariaDB bind address, i.e. the IP address that we want the database server to listen to. We use the internal, private IPv4 address for this. When using the IPv6 network in NREC, instances also get a private IPv4 address. We can use this address for communication between instances, which in our case will be communication between the web servers and the database.
Make sure that the database service is enabled and running.
Install the MySQL bindings for Python. This is needed if we want to use Ansible to communicate with the database server, e.g. for creating databases.
The db.yaml
also includes a handler for restarting MariaDB if we
have done configuration changes which require a restart to take
effect.
The Ansible playbooks above would be run like this, from the Terraform workspace directory:
$ ansible-playbook -i terraform.yaml db.yaml
$ ansible-playbook -i terraform.yaml web.yaml
Complete example¶
A complete listing of the example files used in this document is provided below.
1plugin: cloud.terraform.terraform_provider
2project_path: /path/to/project
3# Terraform binary (available in the $PATH) or full path to the binary.
4binary_path: terraform
1# Define required providers
2terraform {
3 required_version = ">= 1.0"
4 required_providers {
5 openstack = {
6 source = "terraform-provider-openstack/openstack"
7 }
8 ansible = {
9 version = "~> 1.3.0"
10 source = "ansible/ansible"
11 }
12 }
13}
14
15# Configure the OpenStack Provider
16# Empty means using environment variables "OS_*". More info:
17# https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs
18provider "openstack" {}
19
20# SSH key
21resource "openstack_compute_keypair_v2" "keypair" {
22 region = var.region
23 name = "${terraform.workspace}-${var.name}"
24 public_key = file(var.ssh_public_key)
25}
26
27# Web servers
28resource "openstack_compute_instance_v2" "web_instance" {
29 region = var.region
30 count = lookup(var.role_count, "web", 0)
31 name = "${var.region}-web-${count.index}"
32 image_name = lookup(var.role_image, "web", "unknown")
33 flavor_name = lookup(var.role_flavor, "web", "unknown")
34
35 key_pair = "${terraform.workspace}-${var.name}"
36 security_groups = [
37 "default",
38 "${terraform.workspace}-${var.name}-ssh",
39 "${terraform.workspace}-${var.name}-web",
40 ]
41
42 network {
43 name = "IPv6"
44 }
45
46 lifecycle {
47 ignore_changes = [image_name,image_id]
48 }
49
50 depends_on = [
51 openstack_networking_secgroup_v2.instance_ssh_access,
52 openstack_networking_secgroup_v2.instance_web_access,
53 ]
54}
55
56# Database servers
57resource "openstack_compute_instance_v2" "db_instance" {
58 region = var.region
59 count = lookup(var.role_count, "db", 0)
60 name = "${var.region}-db-${count.index}"
61 image_name = lookup(var.role_image, "db", "unknown")
62 flavor_name = lookup(var.role_flavor, "db", "unknown")
63
64 key_pair = "${terraform.workspace}-${var.name}"
65 security_groups = [
66 "default",
67 "${terraform.workspace}-${var.name}-ssh",
68 "${terraform.workspace}-${var.name}-db",
69 ]
70
71 network {
72 name = "IPv6"
73 }
74
75 lifecycle {
76 ignore_changes = [image_name,image_id]
77 }
78
79 depends_on = [
80 openstack_networking_secgroup_v2.instance_ssh_access,
81 openstack_networking_secgroup_v2.instance_db_access,
82 ]
83}
84
85# Volume
86resource "openstack_blockstorage_volume_v3" "volume" {
87 name = "database"
88 size = var.volume_size
89}
90
91# Attach volume
92resource "openstack_compute_volume_attach_v2" "attach_vol" {
93 instance_id = openstack_compute_instance_v2.db_instance[0].id
94 volume_id = openstack_blockstorage_volume_v3.volume.id
95}
96
97# Ansible web hosts
98resource "ansible_host" "web" {
99 count = lookup(var.role_count, "web", 0)
100 name = "${var.region}-web-${count.index}"
101 groups = ["osl_web"] # Groups this host is part of
102
103 variables = {
104 ansible_host = trim(openstack_compute_instance_v2.web_instance[count.index].access_ip_v6, "[]")
105 }
106}
107
108# Ansible db hosts
109resource "ansible_host" "db" {
110 count = lookup(var.role_count, "db", 0)
111 name = "${var.region}-db-${count.index}"
112 groups = ["osl_db"] # Groups this host is part of
113
114 variables = {
115 ansible_host = trim(openstack_compute_instance_v2.db_instance[count.index].access_ip_v6, "[]")
116 }
117}
118
119# Ansible web group
120resource "ansible_group" "web_group" {
121 name = "web"
122 children = ["osl_web"]
123 variables = {
124 ansible_user = "almalinux"
125 }
126}
127
128# Ansible db group
129resource "ansible_group" "db_group" {
130 name = "db"
131 children = ["osl_db"]
132 variables = {
133 ansible_user = "ubuntu"
134 }
135}
1# Security group SSH + ICMP
2resource "openstack_networking_secgroup_v2" "instance_ssh_access" {
3 region = var.region
4 name = "${terraform.workspace}-${var.name}-ssh"
5 description = "Security group for allowing SSH and ICMP access"
6}
7
8# Security group HTTP
9resource "openstack_networking_secgroup_v2" "instance_web_access" {
10 region = var.region
11 name = "${terraform.workspace}-${var.name}-web"
12 description = "Security group for allowing HTTP access"
13}
14
15# Security group MySQL
16resource "openstack_networking_secgroup_v2" "instance_db_access" {
17 region = var.region
18 name = "${terraform.workspace}-${var.name}-db"
19 description = "Security group for allowing MySQL access"
20}
21
22# Allow ssh from IPv4 net
23resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv4" {
24 region = var.region
25 count = length(var.allow_ssh_from_v4)
26 direction = "ingress"
27 ethertype = "IPv4"
28 protocol = "tcp"
29 port_range_min = 22
30 port_range_max = 22
31 remote_ip_prefix = var.allow_ssh_from_v4[count.index]
32 security_group_id = openstack_networking_secgroup_v2.instance_ssh_access.id
33}
34
35# Allow ssh from IPv6 net
36resource "openstack_networking_secgroup_rule_v2" "rule_ssh_access_ipv6" {
37 region = var.region
38 count = length(var.allow_ssh_from_v6)
39 direction = "ingress"
40 ethertype = "IPv6"
41 protocol = "tcp"
42 port_range_min = 22
43 port_range_max = 22
44 remote_ip_prefix = var.allow_ssh_from_v6[count.index]
45 security_group_id = openstack_networking_secgroup_v2.instance_ssh_access.id
46}
47
48# Allow icmp from IPv4 net
49resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv4" {
50 region = var.region
51 count = length(var.allow_ssh_from_v4)
52 direction = "ingress"
53 ethertype = "IPv4"
54 protocol = "icmp"
55 remote_ip_prefix = var.allow_ssh_from_v4[count.index]
56 security_group_id = openstack_networking_secgroup_v2.instance_ssh_access.id
57}
58
59# Allow icmp from IPv6 net
60resource "openstack_networking_secgroup_rule_v2" "rule_icmp_access_ipv6" {
61 region = var.region
62 count = length(var.allow_ssh_from_v6)
63 direction = "ingress"
64 ethertype = "IPv6"
65 protocol = "ipv6-icmp"
66 remote_ip_prefix = var.allow_ssh_from_v6[count.index]
67 security_group_id = openstack_networking_secgroup_v2.instance_ssh_access.id
68}
69
70# Allow HTTP from IPv4 net
71resource "openstack_networking_secgroup_rule_v2" "rule_http_access_ipv4" {
72 region = var.region
73 count = length(var.allow_http_from_v4)
74 direction = "ingress"
75 ethertype = "IPv4"
76 protocol = "tcp"
77 port_range_min = 80
78 port_range_max = 80
79 remote_ip_prefix = var.allow_http_from_v4[count.index]
80 security_group_id = openstack_networking_secgroup_v2.instance_web_access.id
81}
82
83# Allow HTTP from IPv6 net
84resource "openstack_networking_secgroup_rule_v2" "rule_http_access_ipv6" {
85 region = var.region
86 count = length(var.allow_http_from_v6)
87 direction = "ingress"
88 ethertype = "IPv6"
89 protocol = "tcp"
90 port_range_min = 80
91 port_range_max = 80
92 remote_ip_prefix = var.allow_http_from_v6[count.index]
93 security_group_id = openstack_networking_secgroup_v2.instance_web_access.id
94}
95
96# Allow MySQL from IPv4 net
97resource "openstack_networking_secgroup_rule_v2" "rule_mysql_access_ipv4" {
98 region = var.region
99 count = length(var.allow_mysql_from_v4)
100 direction = "ingress"
101 ethertype = "IPv4"
102 protocol = "tcp"
103 port_range_min = 3306
104 port_range_max = 3306
105 remote_ip_prefix = var.allow_mysql_from_v4[count.index]
106 security_group_id = openstack_networking_secgroup_v2.instance_db_access.id
107}
108
109# Allow MYSQL from IPv6 net
110resource "openstack_networking_secgroup_rule_v2" "rule_mysql_access_ipv6" {
111 region = var.region
112 count = length(var.allow_mysql_from_v6)
113 direction = "ingress"
114 ethertype = "IPv6"
115 protocol = "tcp"
116 port_range_min = 3306
117 port_range_max = 3306
118 remote_ip_prefix = var.allow_mysql_from_v6[count.index]
119 security_group_id = openstack_networking_secgroup_v2.instance_db_access.id
120}
121
122# Allow MYSQL from web servers (IPv4)
123resource "openstack_networking_secgroup_rule_v2" "rule_mysql_from_web_access_ipv4" {
124 region = var.region
125 count = 1
126 direction = "ingress"
127 ethertype = "IPv4"
128 protocol = "tcp"
129 port_range_min = 3306
130 port_range_max = 3306
131 remote_group_id = openstack_networking_secgroup_v2.instance_web_access.id
132 security_group_id = openstack_networking_secgroup_v2.instance_db_access.id
133}
134
135# Allow MYSQL from web servers (IPv6)
136resource "openstack_networking_secgroup_rule_v2" "rule_mysql_from_web_access_ipv6" {
137 region = var.region
138 count = 1
139 direction = "ingress"
140 ethertype = "IPv6"
141 protocol = "tcp"
142 port_range_min = 3306
143 port_range_max = 3306
144 remote_group_id = openstack_networking_secgroup_v2.instance_web_access.id
145 security_group_id = openstack_networking_secgroup_v2.instance_db_access.id
146}
1# Variables
2variable "region" {
3}
4
5variable "name" {
6 default = "myproject"
7}
8
9variable "ssh_public_key" {
10 default = "~/.ssh/id_rsa.pub"
11}
12
13variable "network" {
14 default = "IPv6"
15}
16
17variable "volume_size" {
18 default = 20
19}
20
21# Security group defaults
22variable "allow_ssh_from_v6" {
23 type = list(string)
24 default = []
25}
26
27variable "allow_ssh_from_v4" {
28 type = list(string)
29 default = []
30}
31
32variable "allow_http_from_v6" {
33 type = list(string)
34 default = []
35}
36
37variable "allow_http_from_v4" {
38 type = list(string)
39 default = []
40}
41
42variable "allow_mysql_from_v6" {
43 type = list(string)
44 default = []
45}
46
47variable "allow_mysql_from_v4" {
48 type = list(string)
49 default = []
50}
51
52# Mapping between role and image
53variable "role_image" {
54 type = map(string)
55 default = {
56 "web" = "GOLD Alma Linux 9"
57 "db" = "GOLD Ubuntu 24.04 LTS"
58 }
59}
60
61# Mapping between role and flavor
62variable "role_flavor" {
63 type = map(string)
64 default = {
65 "web" = "m1.small"
66 "db" = "m1.medium"
67 }
68}
69
70# Mapping between role and number of instances (count)
71variable "role_count" {
72 type = map(string)
73 default = {
74 "web" = 4
75 "db" = 1
76 }
77}
1# Region
2region = "osl"
3
4# This is needed to access the instance over ssh
5allow_ssh_from_v6 = [
6 "2001:700:100:8070::/64",
7 "2001:700:100:8071::/64",
8]
9allow_ssh_from_v4 = [
10 "129.240.114.32/28",
11 "129.240.114.48/28"
12]
13
14# This is needed to access the instance over http
15allow_http_from_v6 = [
16 "2001:700:200::/48",
17 "2001:700:100::/41"
18]
19allow_http_from_v4 = [
20 "129.177.0.0/16",
21 "129.240.0.0/16"
22]
23
24# This is needed to access the instance over the mysql port
25allow_mysql_from_v6 = [
26 "2001:700:100:4003::43/128"
27]
28allow_mysql_from_v4 = [
29 "129.240.130.12/32"
30]
1---
2- hosts: web
3 become: true
4
5 tasks:
6 - name: Install Apache
7 ansible.builtin.yum:
8 name: httpd
9
10 - name: Install php
11 ansible.builtin.yum:
12 name: php
13
14 - name: Install php-mysqlnd
15 ansible.builtin.yum:
16 name: php-mysqlnd
17
18 - name: Set httpd_can_network_connect_db SELinux boolean
19 ansible.builtin.seboolean:
20 name: httpd_can_network_connect_db
21 state: yes
22 persistent: yes
23
24 - name: Make sure Apache is running and enabled
25 ansible.builtin.systemd:
26 name: httpd
27 state: started
28 enabled: yes
1---
2- hosts: db
3 become: true
4
5 tasks:
6 - name: Create XFS filesystem on /dev/sdb
7 ansible.builtin.filesystem:
8 fstype: xfs
9 dev: /dev/sdb
10
11 - name: Mount volume
12 ansible.builtin.mount:
13 path: /var/lib/mysql
14 src: /dev/sdb
15 fstype: xfs
16 state: mounted
17
18 - name: Install MariaDB, also starts the service
19 ansible.builtin.apt:
20 name: mariadb-server
21 update_cache: yes
22
23 - name: Set MariaDB bind-address
24 ansible.builtin.ini_file:
25 path: /etc/mysql/mariadb.conf.d/90-server.cnf
26 section: mysqld
27 option: bind-address
28 value: "{{ ansible_default_ipv4.address }}"
29 backup: yes
30 notify:
31 - restart MariaDB
32
33 - name: Make sure MariaDB is running and enabled
34 ansible.builtin.systemd:
35 name: mariadb
36 state: started
37 enabled: yes
38
39 - name: Install PyMySQL, needed by Ansible MySQL module
40 ansible.builtin.apt:
41 name: python3-pymysql
42
43
44 handlers:
45 - name: restart MariaDB
46 ansible.builtin.systemd:
47 name: "mariadb"
48 state: restarted