This guide describes how to manage Infrastructure as Code by using Terraform.
In this guide, we will dive into a typical Terraform project setup, exploring each file and its role in the broader context of infrastructure automation on Virtuozzo Infrastructure. Whether you are new to Terraform or looking to enhance your understanding of best practices in file organization and usage, this guide will provide valuable insights into crafting and managing your Terraform configurations effectively.
Prerequisites
An OS supported by Terraform. In this guide, we are using CentOS Stream 9 as the workstation.
A project in a Virtuozzo Infrastructure domain. Ensure that you have the credentials to access this project.
Your favorite text editor. In this guide, we are using Vim.
Note: You need to change the variables used in this guide, such as image or flavor names, to match your environment.
Overview
Our deployment will consist of three Apache web servers behind a load balancer and a database server with MariaDB installed. We will learn how to automatically create all the necessary resources required for this deployment by using Terraform:
Creating VXLANs (private networks for your VMs)
Configuring security groups
Creating and configuring VMs:
Creating network interfaces
Creating boot volumes
Attaching additional volumes
Installing packages
Creating configuration files
Running commands automatically on the first boot
Creating and configuring a load balancer to leverage 3 web instances
Adding a floating IP address to your load balancer
As a result, the directory where you will be working should contain the following items:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── 00-variables.tf # Variables that we will define for our deployment
├── 010-ssh-key.tf # This file will hold your public SSH key to access the environments
├── 020-network.tf # Network creation/configuration
├── 030-security_group.tf # Security Groups creation
├── 060-instance_http.tf # Web server instances definition
├── 061-instance_db.tf # DB instance definition
├── 070-loadbalancer.tf # Load balancer definition
├── provider.tf # OpenStack provider definition
├── scripts
│ ├── first-boot-server1.yaml # Cloud-config example for web servers 1 to 3
│ ├── first-boot-server2.yaml
│ ├── first-boot-server3.yaml
│ └── mariadb.yml # Cloud-config example for the Maria DB installation
└── secrets.tfvars # File with the password and info about your cloud
Step-by-step guide
1. Create the cloud provider file that will define the modules and terraform provider versions:
3. Create the variables file and change variables to match your current environment. These variables are used globally in each of the definition files, and their file should be placed in the same directory as the the definition file you are working on. There are also other ways to do this, but this approach is much simpler.
cat > 00-variables.tf <<\EOT
variable "os_username" {}
variable "os_tenant_name" {}
variable "os_password" {}
variable "os_auth_url" {}
variable "os_region" {}
variable "os_domain_name" {}
# Params file for variables
#Set here the storage policy name
variable "volume_type" {
type = string
default = "standard" #change this to match the name of your storage policy
}
#### GLANCE
variable "image" {
type = string
default = "CentOS-9" #change this to match the name of the image you'd like to use (must be centos based)
}
#### NEUTRON
variable "external_network" {
type = string
default = "external-network"
}
# UUID of external gateway
variable "external_gateway" {
type = string
default = "26e450e5-e600-4174-a1e1-cc023e850095" # Find your public network on your project. Click on the network and replace the ID
}
variable "dns_ip" {
type = list(string)
default = ["8.8.8.8", "1.1.1.1"] # whatever you like to use here
}
#### VM HTTP parameters ####
variable "flavor_http" {
type = string
default = "va-4-8" # change this to match the available flavor on your cloud
}
variable "network_http" {
type = map(string)
default = {
subnet_name = "subnet-http"
cidr = "192.168.1.0/24"
}
}
variable "http_instance_names" {
description = "Map of instance names to their cloud-config filenames"
type = map(string)
default = {
"http-instance-1" = "scripts/first-boot-server1.yaml", #make your you create a subdir called scripts we will populate this later
"http-instance-2" = "scripts/first-boot-server2.yaml",
"http-instance-3" = "scripts/first-boot-server3.yaml"
}
}
#### MAIN DISK SIZE FOR HTTP
variable "volume_http" {
type = number
default = 10
}
#### VM DB parameters ####
variable "flavor_db" {
type = string
default = "va-4-8" # change this to match the available flavor on your cloud
}
variable "network_db" {
type = map(string)
default = {
subnet_name = "subnet-db"
cidr = "192.168.2.0/24"
}
}
variable "db_instance_names" {
type = set(string)
default = ["db-instance-1"]
}
#### ATTACHED VOLUME PARAMS
variable "volume_db" {
type = number
default = 15
}
EOT
4. Create the SSH key file that will define the key to be added to workloads. You need to specify your public SSH key.
1
2
3
4
5
6
7
# cat > 010-ssh-key.tf <<\EOT
#Define ssh to config in instance
resource "openstack_compute_keypair_v2" "user_key" {
name = "user-key"
public_key = "ssh-rsa AAAAB3NzaC......" # Your public SSH key here
}
EOT
5. Create the network configuration for your VMs that will define a network, a router, and two subnets: one is for the HTTP servers (web servers), the other is for the MariaDB server.
# cat > 070-loadbalancer.tf <<\EOT
# HTTP LOAD BALANCER CONFIGURATION
#
# Create load balancer
resource "openstack_lb_loadbalancer_v2" "http" {
name = "elastic_loadbalancer_http"
vip_subnet_id = openstack_networking_subnet_v2.http.id
depends_on = [openstack_compute_instance_v2.http]
}
# Create listener
resource "openstack_lb_listener_v2" "http" {
name = "listener_http"
protocol = "TCP"
protocol_port = 80
loadbalancer_id = openstack_lb_loadbalancer_v2.http.id
depends_on = [openstack_lb_loadbalancer_v2.http]
}
# Set method for load balancer change between instances
resource "openstack_lb_pool_v2" "http" {
name = "pool_http"
protocol = "TCP"
lb_method = "ROUND_ROBIN"
listener_id = openstack_lb_listener_v2.http.id
depends_on = [openstack_lb_listener_v2.http]
}
# Add multiple instances to pool
resource "openstack_lb_member_v2" "http" {
for_each = var.http_instance_names
address = openstack_compute_instance_v2.http[each.key].access_ip_v4
protocol_port = 80
pool_id = openstack_lb_pool_v2.http.id
subnet_id = openstack_networking_subnet_v2.http.id
depends_on = [openstack_lb_pool_v2.http]
}
# Create health monitor to check service instance status
resource "openstack_lb_monitor_v2" "http" {
name = "monitor_http"
pool_id = openstack_lb_pool_v2.http.id
type = "TCP"
delay = 2
timeout = 2
max_retries = 2
depends_on = [openstack_lb_member_v2.http]
}
# Create floating IP for http load balancer
resource "openstack_networking_floatingip_v2" "http" {
pool = "Public"
}
# Associate
resource "openstack_networking_floatingip_associate_v2" "http_fip_assoc" {
floating_ip = openstack_networking_floatingip_v2.http.address
fixed_ip = openstack_lb_loadbalancer_v2.http.vip_address
port_id = openstack_lb_loadbalancer_v2.http.vip_port_id
}
# DB LOAD BALANCER CONFIGURATION
#
# Create load balancer
resource "openstack_lb_loadbalancer_v2" "db" {
name = "elastic_loadbalancer_db"
vip_subnet_id = openstack_networking_subnet_v2.db.id
depends_on = [openstack_compute_instance_v2.db]
}
# Create listener
resource "openstack_lb_listener_v2" "db" {
name = "listener_db"
protocol = "TCP"
protocol_port = 3306
loadbalancer_id = openstack_lb_loadbalancer_v2.db.id
depends_on = [openstack_lb_loadbalancer_v2.db]
}
# Set method for load balancer change between instances
resource "openstack_lb_pool_v2" "db" {
name = "pool_db"
protocol = "TCP"
lb_method = "ROUND_ROBIN"
listener_id = openstack_lb_listener_v2.db.id
depends_on = [openstack_lb_listener_v2.db]
}
# Add multiple instances to pool
resource "openstack_lb_member_v2" "db" {
for_each = var.db_instance_names
address = openstack_compute_instance_v2.db[each.key].access_ip_v4
protocol_port = 3306
pool_id = openstack_lb_pool_v2.db.id
subnet_id = openstack_networking_subnet_v2.db.id
depends_on = [openstack_lb_pool_v2.db]
}
# Create health monitor to check service instance status
resource "openstack_lb_monitor_v2" "db" {
name = "monitor_db"
pool_id = openstack_lb_pool_v2.db.id
type = "TCP"
delay = 2
timeout = 2
max_retries = 2
depends_on = [openstack_lb_member_v2.db]
}
EOT
10. Create the configuration files with cloud-init (cloud-config) that will configure your instances on the first boot:
Important: The directory name should be scripts, as it is referenced in the instance definitions for the database and web server files. All of the YAML definitions should be located inside the scripts directory.
10.1. Create the configuration file for the first web server inside the scripts directory:
mkdir scripts
cd scripts
cat > first-boot-server1.yaml<<\EOT
#cloud-config
packages:
- httpd
- policycoreutils-python-utils
- mariadb # we need this to test access to the mariadb server later
runcmd:
# Update all packages first
#- [ dnf, update, -y ] #uncomment this line if you have time. It takes time to install packages
# Enable and start Apache
- [ systemctl, enable, httpd ]
- [ systemctl, start, httpd ]
# Configure firewall to allow HTTP and HTTPS traffic
- [ firewall-cmd, --permanent, --add-service=http ]
- [ firewall-cmd, --reload ]
# Configure SELinux to allow Apache to serve content
- [ semanage, fcontext, -a, -t, httpd_sys_content_t, "/var/www/html(/.*)?" ]
- [ restorecon, -Rv, /var/www/html ]
# Restart Apache to apply all changes
- [ systemctl, restart, httpd ]
# This will write a basic test page to the web root
write_files:
- path: /var/www/html/index.html
content: |
<html>
<head><title>Test Page</title></head>
<body>
<h1>Hello, HTTP World! in server1</h1>
</body>
</html>
owner: root:root
permissions: '0644'
EOT
10.2. Create the configuration file for the second web server:
# cat > first-boot-server2.yaml<<\EOT
#cloud-config
packages:
- httpd
- policycoreutils-python-utils
- mariadb # we need this to test access to the mariadb server later
runcmd:
# Update all packages first
#- [ dnf, update, -y ] #uncomment this line if you have time. It takes time to install packages
# Enable and start Apache
- [ systemctl, enable, httpd ]
- [ systemctl, start, httpd ]
# Configure firewall to allow HTTP and HTTPS traffic
- [ firewall-cmd, --permanent, --add-service=http ]
- [ firewall-cmd, --reload ]
# Configure SELinux to allow Apache to serve content
- [ semanage, fcontext, -a, -t, httpd_sys_content_t, "/var/www/html(/.*)?" ]
- [ restorecon, -Rv, /var/www/html ]
# Restart Apache to apply all changes
- [ systemctl, restart, httpd ]
# This will write a basic test page to the web root
write_files:
- path: /var/www/html/index.html
content: |
<html>
<head><title>Test Page</title></head>
<body>
<h1>Hello, HTTP World! in server2</h1>
</body>
</html>
owner: root:root
permissions: '0644'
EOT
10.3. Create the configuration file for the third web server:
# cat > first-boot-server3.yaml<<\EOT
#cloud-config
packages:
- httpd
- policycoreutils-python-utils
- mariadb # we need this to test access to the mariadb server later
runcmd:
# Update all packages first
#- [ dnf, update, -y ] #uncomment this line if you have time. It takes time to install packages
# Enable and start Apache
- [ systemctl, enable, httpd ]
- [ systemctl, start, httpd ]
# Configure firewall to allow HTTP and HTTPS traffic
- [ firewall-cmd, --permanent, --add-service=http ]
- [ firewall-cmd, --reload ]
# Configure SELinux to allow Apache to serve content
- [ semanage, fcontext, -a, -t, httpd_sys_content_t, "/var/www/html(/.*)?" ]
- [ restorecon, -Rv, /var/www/html ]
# Restart Apache to apply all changes
- [ systemctl, restart, httpd ]
# This will write a basic test page to the web root
write_files:
- path: /var/www/html/index.html
content: |
<html>
<head><title>Test Page</title></head>
<body>
<h1>Hello, HTTP World! in server3</h1>
</body>
</html>
owner: root:root
permissions: '0644'
EOT
10.4. Create the configuration file for the database:
# cat > mariadb.yml <<\EOT
#cloud-config
packages:
- mariadb-server
- mariadb
- firewalld
- policycoreutils-python-utils # Ensure semanage is available
runcmd:
# Update the system
#- [ dnf, update, -y ] #uncomment this line if you have time. It takes time to install packages
# Enable and start MariaDB and firewalld
- [ systemctl, enable, --now, mariadb ]
- [ systemctl, enable, --now, firewalld ]
# Configure MariaDB to listen on all interfaces
- echo "[mysqld]" >> /etc/my.cnf.d/mariadb-server.cnf
- echo "bind-address=0.0.0.0" >> /etc/my.cnf.d/mariadb-server.cnf
# Restart MariaDB to apply configuration changes
- [ systemctl, restart, mariadb ]
# Secure the installation
- [ mysql, -e, "SET PASSWORD FOR 'root'@'localhost' = PASSWORD('yourStrongPasswordHere'); FLUSH PRIVILEGES;" ]
- [ mysql, -e, "DELETE FROM mysql.user WHERE User = ''; FLUSH PRIVILEGES;" ]
- [ mysql, -e, "DELETE FROM mysql.user WHERE User = 'root' AND Host NOT IN ('localhost', '127.0.0.1', '::1'); FLUSH PRIVILEGES;" ]
- [ mysql, -e, "DROP DATABASE IF EXISTS test; FLUSH PRIVILEGES;" ]
- [ mysql, -e, "CREATE USER 'remote_user'@'%' IDENTIFIED BY 'anotherStrongPassword';" ]
- [ mysql, -e, "GRANT ALL PRIVILEGES ON *.* TO 'remote_user'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;" ]
# Open firewall for remote connections
- [ firewall-cmd, --permanent, --add-port=3306/tcp ]
- [ firewall-cmd, --reload ]
# Adjust SELinux to allow MariaDB to accept remote connections
- [ semanage, port, -a, -t, mysqld_port_t, -p, tcp, 3306 ]
- [ setsebool, -P, mysql_connect_any, 1 ]
final_message: "MariaDB server setup is complete and running on: $public_ipv4"
EOT
Your directory should have the following contents:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── 00-variables.tf # Variables that we will define for our deployment
├── 010-ssh-key.tf # This file will hold your public SSH key to access the environments
├── 020-network.tf # Network creation/configuration
├── 030-security_group.tf # Security Groups creation
├── 060-instance_http.tf # Web server instances definition
├── 061-instance_db.tf # DB instance definition
├── 070-loadbalancer.tf # Load balancer definition
├── provider.tf # OpenStack provider definition
├── scripts
│ ├── first-boot-server1.yaml # Cloud-config example for web servers 1 to 3
│ ├── first-boot-server2.yaml
│ ├── first-boot-server3.yaml
│ └── mariadb.yml # Cloud-config example for the Maria DB installation
└── secrets.tfvars # File with the password and info about your cloud
11. The configuration scripts created for the web servers will install HTTPS (Apache on CentOS). Update the system, enable and start the Apache service, configure the firewall, and add the index.html file with a different message to each web server. When testing the load balancer, this message will show us that we are in the round-robin mode.
12. Install, configure, and enable MariaDB on the database server.
Now, you are ready to run Terraform commands to deploy the defined Infrastructure as Code in to your Virtuozzo Infrastructure project.
1. Initialize Terraform:
1
# terraform init
The command output should be similar to the following:
2. Once Terraform is successfully initialized, run the terraform plan command:
1
# terraform plan -var-file="secrets.tfvars"
3. If the previous command was successful, proceed with the deployment:
1
# terraform apply -var-file="secrets.tfvars"
The command will create the following resources:
Now, when you access your web servers at the load balancer public IP address, you will see the following:
Finally, add a floating IP address to one of your web servers and try to access MariaDB on the database server (the password is anotherStrongPassword):