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:

main.tf
 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:

main.tf
 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”:

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:

web.yaml
 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:

db.yaml
 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.

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
main.tf
  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}
secgroup.tf
  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}
variables.tf
 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}
terraform.tfvars
 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]
web.yaml
 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
db.yaml
 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