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.
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:
| Variable | Description |
|---|---|
name | Instance hostname |
image_id | Glance image UUID |
flavor_id | Nova flavor UUID or name |
key_pair | SSH keypair name |
network_id | Neutron network UUID |
fixed_ip | Static IP on the network (null for DHCP) |
assign_floating_ip | Assign a public floating IP (true/false) |
security_groups | List of security group IDs |
userdata_file | Path to cloud-init file (null to skip) |
metadata_role | Instance role tag for metadata |
volumes | List 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.