Floating IPs within OpenStack are important to ensure that there is proper external connectivity for virtual machine instances that are set to run within private networks. The Floating IPs are the key channels of communication that enable the virtual machine to connect with the world outside, including not only the crucial internet connect but also access to other external services that can be necessary.
In this how-to guide, you will discover all the steps required to provision a Nova compute instance with a floating IP address using Terraform by leveraging the very handy nova_floating module to do so.
Prerequisites
Ensure the following before continuing:
- Terraform is installed.
- OpenStack CLI credentials are set or sourced.
- You have access to an external network in your OpenStack project.
- You’ve created a private network and a subnet.
- You have a Glance image and an SSH keypair already uploaded.
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: The Instance 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
Configure your main.tf
file as follows:
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 = module.keypair.keypair_name
network_id = module.network.network_id
fixed_ip = "172.34.50.11"
assign_floating_ip = true
security_groups = [module.security_group.security_group_id]
userdata_file = null
metadata_role = "web-server"
volumes = []
}
]
}
If you prefer to use DHCP instead of assigning a static ip, set fixed_ip
to null.
Apply the configuration
Deploy your cloud infrastructure using the commands below:
terraform init
terraform plan
terraform apply