Create Nova VM Instances With Cinder Volumes using Terraform

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:

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 info
  • main.tf – creates the server and attaches a floating IP
  • variables.tf – defines inputs
  • outputs.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:

Join our Linux and open source community. Subscribe to our newsletter for tips, tricks, and collaboration opportunities!

Recent Post

Unlock the Right Solutions with Confidence

At CloudSpinx, we don’t just offer services - we deliver clarity, direction, and results. Whether you're navigating cloud adoption, scaling infrastructure, or solving DevOps challenges, our seasoned experts help you make smart, strategic decisions with total confidence. Let us turn complexity into opportunity and bring your vision to life.

Leave a Comment

Your email address will not be published. Required fields are marked *

Related Post

Autofs also referred to as automount is a feature in Linux like systems that automatically mount filesystems on demand. In […]

Every Linux user must be familiar with the command ‘man’. The command is used to display instructions on the usage […]

Engineering is a science-math problem solving field in our society. Engineers make use of engineering skills to convert ideas in […]

Let's Connect

Unleash the full potential of your business with CloudSpinx. Our expert solutions specialists are standing by to answer your questions and tailor a plan that perfectly aligns with your unique needs.
You will get a response from our solutions specialist within 12 hours
We understand emergencies can be stressful. For immediate assistance, chat with us now

Contact CloudSpinx today!

Download CloudSpinx Profile

Discover the full spectrum of our expertise and services by downloading our detailed Company Profile. Simply enter your first name, last name, and email address.