Cloud

Deploy VMs on OpenStack with Terraform Modules

Terraform automates infrastructure provisioning on OpenStack through declarative configuration files. Instead of manually creating instances, networks, and volumes through the Horizon dashboard or CLI, you define the desired state in code and let Terraform handle the rest.

Original content from computingforgeeks.com - post 81765

This guide uses the cloudspinx/terraform-openstack module – a set of reusable Terraform modules for managing OpenStack resources including instances, networks, keypairs, flavors, security groups, and Glance images. We cover both plain Terraform and Terragrunt workflows.

Prerequisites

  • A working OpenStack deployment (Packstack or Kolla-Ansible)
  • Terraform or OpenTofu installed (v1.2+)
  • OpenStack API credentials (auth URL, username, password, project name)
  • An SSH keypair for instance access

Step 1: Configure the OpenStack Provider

Create a project directory and define the OpenStack provider configuration. This tells Terraform how to authenticate with your OpenStack API.

mkdir ~/openstack-terraform && cd ~/openstack-terraform

Create the provider configuration file:

vi provider.tf

Add the provider block with your OpenStack credentials:

terraform {
  required_version = ">= 1.2"
  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 2.1.0"
    }
  }
}

provider "openstack" {
  user_name   = "admin"
  tenant_name = "admin"
  password    = "YourAdminPassword"
  auth_url    = "http://192.168.1.100:5000/v3"
  region      = "RegionOne"
}

Replace the credentials with your actual OpenStack API details. You can find the auth URL and credentials in your keystonerc_admin or admin-openrc.sh file.

Step 2: Upload an Image to Glance

Before creating instances, you need a cloud image in OpenStack Glance. The module handles the upload for you.

Download a cloud image first:

wget https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img

Create a Terraform file to upload it:

vi glance.tf

Define the Glance image module:

module "glance_image" {
  source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/glance_image?ref=main"

  image_name       = "Ubuntu-24.04"
  disk_format      = "qcow2"
  container_format = "bare"
  visibility       = "public"
  local_file_path  = "./noble-server-cloudimg-amd64.img"
}

Step 3: Create SSH Keypair

Create a file for the keypair module:

vi keypair.tf

Define the keypair using your existing public key:

module "keypair" {
  source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/keypair?ref=main"

  keypair_name = "mykey"
  public_key   = file("~/.ssh/id_rsa.pub")
}

Step 4: Create Compute Flavors

Flavors define the CPU, RAM, and disk size for instances. Skip this step if your OpenStack already has flavors configured.

vi flavors.tf

Define multiple flavors in a single module call:

module "flavors" {
  source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/flavor?ref=main"

  flavors = [
    {
      name      = "small"
      ram       = 2048
      vcpus     = 1
      disk      = 20
      swap      = 0
      is_public = true
    },
    {
      name      = "medium"
      ram       = 4096
      vcpus     = 2
      disk      = 40
      swap      = 0
      is_public = true
    },
    {
      name      = "large"
      ram       = 8192
      vcpus     = 4
      disk      = 80
      swap      = 0
      is_public = true
    }
  ]
}

Step 5: Create a Private Network

Instances need a private network to communicate. Create the network configuration file:

vi network.tf

Define a private network with a subnet and router connected to the external network:

module "network" {
  source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/network?ref=main"

  network_name       = "private-net"
  subnet_name        = "private-subnet"
  subnet_cidr        = "172.16.0.0/24"
  dns_nameservers    = ["8.8.8.8", "8.8.4.4"]
  router_name        = "private-router"
  external_network_id = "YOUR_EXTERNAL_NETWORK_ID"
}

Get your external network ID with:

openstack network list --external

Step 6: Create a Security Group

Security groups control inbound and outbound traffic to instances. Create a security group that allows SSH and ICMP:

vi security.tf

Define a security group that allows SSH and ICMP:

module "security_group" {
  source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/security_group?ref=main"

  security_group_name        = "allow-ssh-icmp"
  security_group_description = "Allow SSH and ICMP"

  rules = [
    {
      direction        = "ingress"
      ethertype        = "IPv4"
      protocol         = "tcp"
      port_range_min   = 22
      port_range_max   = 22
      remote_ip_prefix = "0.0.0.0/0"
    },
    {
      direction        = "ingress"
      ethertype        = "IPv4"
      protocol         = "icmp"
      port_range_min   = 0
      port_range_max   = 0
      remote_ip_prefix = "0.0.0.0/0"
    }
  ]
}

Step 7: Deploy a VM Instance

This is the main step – creating an instance using the resources defined above.

vi instance.tf

Define the instance module. It references the keypair, flavor, network, and security group modules created earlier:

module "instance" {
  source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/instance?ref=main"

  floating_ip_pool = "public"
  instances = [
    {
      name               = "web-server-01"
      image_id           = module.glance_image.image_id
      flavor_id          = module.flavors.flavor_ids["medium"]
      key_pair           = module.keypair.keypair_name
      network_id         = module.network.network_id
      fixed_ip           = null
      assign_floating_ip = true
      security_groups    = [module.security_group.security_group_id]
      userdata_file      = null
      metadata_role      = "web-server"
      volumes            = []
    }
  ]
}

Instance with Cinder Volumes

To attach block storage volumes to the instance, add entries to the volumes list:

module "instance_with_storage" {
  source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/instance?ref=main"

  floating_ip_pool = "public"
  instances = [
    {
      name               = "db-server-01"
      image_id           = module.glance_image.image_id
      flavor_id          = module.flavors.flavor_ids["large"]
      key_pair           = module.keypair.keypair_name
      network_id         = module.network.network_id
      fixed_ip           = "172.16.0.50"
      assign_floating_ip = true
      security_groups    = [module.security_group.security_group_id]
      userdata_file      = null
      metadata_role      = "database"
      volumes = [
        { volume_size = 100 },
        { volume_size = 50 }
      ]
    }
  ]
}

This creates the instance with two Cinder volumes (100GB and 50GB) attached automatically.

Multiple Instances in One Module Call

Deploy several instances at once by adding more entries to the instances list:

module "cluster" {
  source = "git::https://github.com/cloudspinx/terraform-openstack.git//modules/instance?ref=main"

  floating_ip_pool = "public"
  instances = [
    {
      name               = "k8s-master"
      image_id           = module.glance_image.image_id
      flavor_id          = module.flavors.flavor_ids["large"]
      key_pair           = module.keypair.keypair_name
      network_id         = module.network.network_id
      fixed_ip           = "172.16.0.10"
      assign_floating_ip = true
      security_groups    = [module.security_group.security_group_id]
      userdata_file      = null
      metadata_role      = "k8s-master"
      volumes            = []
    },
    {
      name               = "k8s-worker-01"
      image_id           = module.glance_image.image_id
      flavor_id          = module.flavors.flavor_ids["medium"]
      key_pair           = module.keypair.keypair_name
      network_id         = module.network.network_id
      fixed_ip           = "172.16.0.11"
      assign_floating_ip = false
      security_groups    = [module.security_group.security_group_id]
      userdata_file      = null
      metadata_role      = "k8s-worker"
      volumes            = []
    },
    {
      name               = "k8s-worker-02"
      image_id           = module.glance_image.image_id
      flavor_id          = module.flavors.flavor_ids["medium"]
      key_pair           = module.keypair.keypair_name
      network_id         = module.network.network_id
      fixed_ip           = "172.16.0.12"
      assign_floating_ip = false
      security_groups    = [module.security_group.security_group_id]
      userdata_file      = null
      metadata_role      = "k8s-worker"
      volumes            = []
    }
  ]
}

Step 8: Initialize and Apply

Initialize Terraform to download the provider and modules:

terraform init

Preview the changes Terraform will make:

terraform plan

The plan output shows every resource that will be created – instances, volumes, floating IPs, networks, and security groups. Review it carefully before applying.

Apply the configuration to create all resources:

terraform apply -auto-approve

Terraform creates the resources in dependency order – network first, then security groups, then instances. The output shows the instance IPs and volume attachments:

Apply complete! Resources: 12 added, 0 changed, 0 destroyed.

Outputs:

instance_names_and_ips = {
  "0" = {
    "access_ip_v4" = "172.16.0.50"
    "fixed_ip" = "172.16.0.50"
    "floating_ip" = "192.168.200.110"
    "name" = "web-server-01"
  }
}

Step 9: Connect to the Instance

SSH into the instance using the floating IP from the output:

ssh -i ~/.ssh/id_rsa [email protected]

Managing Infrastructure

Check the current state of all managed resources:

terraform state list

Destroy all resources when no longer needed:

terraform destroy -auto-approve

Module Variables Reference

The instance module accepts the following variables for each instance in the list:

VariableDescription
nameInstance hostname
image_idGlance image UUID
flavor_idNova flavor UUID or name
key_pairSSH keypair name
network_idNeutron network UUID
fixed_ipStatic IP on the network (null for DHCP)
assign_floating_ipAssign a public floating IP (true/false)
security_groupsList of security group IDs
userdata_filePath to cloud-init file (null to skip)
metadata_roleInstance role tag for metadata
volumesList of Cinder volumes to attach (empty list for none)

Conclusion

The cloudspinx/terraform-openstack module simplifies OpenStack resource management by providing reusable, tested modules for instances, networks, flavors, keypairs, and security groups. All module source code is on GitHub – contributions and issues are welcome. For managing OpenStack networks with Terraform, see our dedicated guide.

Related Articles

Cloud Use Vault-Agent sidecar to inject Secrets in Vault to Kubernetes Pod AWS Change Server Hostname in EC2 or OpenStack or DigitalOcean or Azure Instance Virtualization Create and Configure Bridge Networking For KVM in Linux Openstack Create Cinder Volumes in OpenStack and Attach to VM

Leave a Comment

Press ESC to close