The OpenStack Block Storage service Cinder is used to supply volumes to containers, Ironic bare metal hosts, Nova virtual machines, and other systems.
Within OpenStack, one of the most embraced and practiced methods includes booting instances from cinder volumes, which has the twofold benefit of offering persistent storage solutions in addition to enabling more efficient and effective disk management. Instead of merely starting virtual machines directly from Glance images, which are basically snapshots of images, you can choose to make a Cinder volume bootable. The bootable volume can subsequently be attached to a Nova instance, thus adding flexibility and usefulness to your cloud environment.
In this guide, we will learn how we can achieve that using our terraform-openstak instance module.
Prerequisites
To successfully use the instance module, ensure that:
- Terraform is installed
- You have valid OpenStack credentials
- A Glance image exists in your OpenStack environment
- You’ve created or have access to a private network and security group
- An SSH keypair is created
Step 1: Install Terraform
If you don’t have terraform installed, run one of the following commands that match your working environment:
# Ubuntu/Debian
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# CentOS/RHEL
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
sudo yum -y install terraform
# Fedora
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager addrepo --from-repofile=https://rpm.releases.hashicorp.com/fedora/hashicorp.repo
sudo dnf -y install terraform
# Amazon Linux
sudo yum install -y yum-utils shadow-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum -y install terraform
# macOS Homebrew
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
Step 2: Configure Terraform Provider
To authenticate Terraform with OpenStack, define the provider as follows in your main.tf
file:
# Define required providers
terraform {
required_version = ">= 0.14.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.1.0"
}
}
}
# Configure the OpenStack Provider
provider "openstack" {
user_name = "admin"
tenant_name = "admin"
password = "pwd"
auth_url = "http://myauthurl:5000/v3"
region = "RegionOne"
}
For a local statefile, you can configure it as follows:
terraform {
backend "local" {
path = "${path.module}/terraform.tfstate"
}
}
Step 3: Module Overview
The instance
module contains the following config files:
data.tf
– retrieves network and image infomain.tf
– creates the server and attaches a floating IPvariables.tf
– defines inputsoutputs.tf
– exposes the floating IP and instance ID
data.info
This file fetches network information to be used in configuring the VM:
# Fetch network details
data "openstack_networking_network_v2" "network" {
name = var.floating_ip_pool
}
main.tf
Provisions the VM and floating IP:
# Define instance creation resource
resource "openstack_compute_instance_v2" "instance" {
for_each = { for idx, instance in var.instances : idx => instance }
name = each.value.name
image_id = each.value.image_id
flavor_id = each.value.flavor_id
key_pair = each.value.key_pair
security_groups = each.value.security_groups
network {
uuid = each.value.network_id
fixed_ip_v4 = each.value.fixed_ip != "" ? each.value.fixed_ip : null
}
# Conditionally include userdata if provided
user_data = each.value.userdata_file != null ? file(each.value.userdata_file) : null
metadata = {
role = each.value.metadata_role
}
}
# Flatten the volume definitions and create a unique map for volumes
locals {
volumes_list = flatten([
for instance_idx, instance in var.instances : [
for volume_idx, volume in lookup(instance, "volumes", []) : {
instance_idx = instance_idx
volume_idx = volume_idx
volume = volume
volume_key = "${instance_idx}-${volume_idx}"
}
]
])
}
# Define volumes
resource "openstack_blockstorage_volume_v3" "volume" {
for_each = { for vol in local.volumes_list : vol.volume_key => vol }
name = "volume-${each.value.instance_idx}-${each.value.volume_idx}-${openstack_compute_instance_v2.instance[each.value.instance_idx].name}"
size = each.value.volume.volume_size
}
# Attach volume(s) to instance
resource "openstack_compute_volume_attach_v2" "volume_attachment" {
for_each = { for vol in local.volumes_list : vol.volume_key => vol }
instance_id = openstack_compute_instance_v2.instance[each.value.instance_idx].id
volume_id = openstack_blockstorage_volume_v3.volume[each.key].id
}
# Null resource to wait for instances creation
resource "null_resource" "wait_for_instances" {
for_each = { for idx, instance in var.instances : idx => instance if instance.assign_floating_ip }
provisioner "local-exec" {
command = "echo Instance ${each.key} created"
}
depends_on = [openstack_compute_instance_v2.instance]
}
# Resource to create floating IPs
resource "openstack_networking_floatingip_v2" "fip" {
for_each = { for idx, instance in var.instances : idx => instance if instance.assign_floating_ip }
pool = var.floating_ip_pool
}
# Resource to associate floating IP
resource "openstack_compute_floatingip_associate_v2" "fip_assoc" {
for_each = { for idx, instance in var.instances : idx => instance if instance.assign_floating_ip }
floating_ip = openstack_networking_floatingip_v2.fip[each.key].address
instance_id = openstack_compute_instance_v2.instance[each.key].id
fixed_ip = openstack_compute_instance_v2.instance[each.key].network[0].fixed_ip_v4
}
variables.tf
The variable file defines the input variables for the module:
variable "instances" {
description = "List of instance configurations or a single instance configuration."
type = list(object({
name = string
image_id = string
flavor_id = string
key_pair = string
network_id = string
security_groups = list(string)
fixed_ip = optional(string)
assign_floating_ip = bool
metadata_role = string
userdata_file = optional(string, null)
volumes = list(object({
volume_size = number
}))
}))
}
variable "floating_ip_pool" {
description = "The name of the floating IP pool"
type = string
default = "public"
}
outputs.tf
Outputs volume attachment information such as instance names and the floating IPs:
# Output volume IDs and names
output "volume_ids_and_names" {
value = { for volume in openstack_blockstorage_volume_v3.volume :
volume.id => volume.name
}
}
# Output volume attachment information with instance names
output "volume_attachments_with_names" {
value = [
for attachment in openstack_compute_volume_attach_v2.volume_attachment :
{
instance_name = lookup({ for inst in openstack_compute_instance_v2.instance : inst.id => inst.name }, attachment.instance_id, "unknown")
volume_id = attachment.volume_id
}
]
}
output "instance_names_and_ips" {
value = { for idx, instance in openstack_compute_instance_v2.instance : idx => {
name = instance.name
fixed_ip = instance.network[0].fixed_ip_v4
access_ip_v4 = instance.access_ip_v4
floating_ip = try(openstack_networking_floatingip_v2.fip[idx].address, null)
}
}
Step 4: Module Usage
To use the module, add the following to your main.tf
file:
module "instance" {
source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/instance?ref=main"
floating_ip_pool = "public"
instances = [
{
name = "instancename"
image_id = "imageid"
flavor_id = module.flavors.flavor_ids["medium"]
key_pair = "keypair"
network_id = module.network.network_id
fixed_ip = null
assign_floating_ip = false
security_groups = [module.security_group.security_group_id]
userdata_file = "./cloud-init.yaml"
metadata_role = "web-server"
volumes = [
{
volume_size = 50
}
]
}
]
}
If you want to attach multiple volumes:
volumes = [
{
volume_size = 50
},
{
volume_size = 50
}
]
Apply the configuration
To deploy the Nova Instance, initialize and apply the terraform configurations:
terraform init
terraform apply
Booting from volumes is a best practice for production workloads since it ensures enhanced performance, flexibility to resize, and data persistence. It is easily achievable using Terraform and our instance module.
Related articles:
- How To Create SSH keypair on OpenStack using Terraform
- Uploading VM Images to OpenStack Glance using Terraform
- How To Create Private Networks in OpenStack using Terraform
- How To Create Security Groups in OpenStack using Terraform
- Create OpenStack Compute Nova Flavors using Terraform
- Create Nova VM Instances With Floating IP using Terraform