Quick Start with Terraform

Published

In this section we will demonstrate, with examples, how to deploy a simple infrastructure in the Peerobyte cloud: create instances, assign floating (external) IPs, and create and attach additional volumes.

Prerequisites

  1. Peerobyte Account.
    You must have a registered account in the Peerobyte client portal. For details, see "Creating an Account".
  2. Cloud Infrastructure Service.
    You must order the “Cloud Infrastructure” service through the Peerobyte client portal and wait for activation confirmation. The notification of infrastructure readiness will be sent to your email or appear in the portal. See "How to Order Cloud Infrastructure" for instructions.
  3. Terraform Installed.
    Terraform must be installed on the machine from which you intend to manage the deployment. For installation instructions, see "Installing Terraform".
  4. Terraform Authorization Configured.
    Create your configuration file and set up Terraform authentication in the Peerobyte cloud as described in "Authentication in Cloud Infrastructure via Terraform".

Before You Begin

During the configuration process, you may need data about the required resources and parameters. You can obtain them via the Horizon control panel or the CLI:

Important: In all configuration examples, replace template values (placeholders) enclosed in angle brackets <…> with the real parameters of your project without the brackets:

  • String value:
    		
    # Original template from example instance_id = "<instance_id>" # After replacement — actual value in quotes instance_id = "45fb7597-50db-4a39-b0f2-b6ce2eca773a"
  • Terraform resource or variable reference:
    		
    # Original template from example instance_id = "<instance_id>" # After replacement — resource reference, without quotes instance_id = openstack_compute_instance_v2.tf_instance_1.id
  • Numeric value:
    		
    # Original template from example volume_size = <volume_size_in_gb> # After replacement — just the number, without quotes volume_size = 30

For deeper study of the OpenStack Provider setup in Terraform, refer to the official HashiCorp documentation.

I. Creating an Instance via Terraform

In this section, we’ll cover creating one or multiple instances in the Peerobyte cloud infrastructure using Terraform. We will build upon the configuration file you created when setting up Terraform authentication as per "Authentication in Cloud Infrastructure via Terraform".

Creating a Single Instance

  1. Add the following instance resource block to your Terraform configuration file:
    		
    # Example block for creating a single instance in OpenStack via Terraform resource "openstack_compute_instance_v2" "tf_instance_1" {   name            = "<instance_name>"        # Name of the instance to create   image_id        = "<os_image_id>"          # ID of the OS image to use   flavor_id       = "<instance_flavor_id>"   # Instance flavor ID (defines CPU, RAM)   key_pair        = "<key_pair_name>"        # SSH key pair name   security_groups = ["default"]              # Security groups to apply   user_data       = file("${path.module}/<cloud-init.yaml>")  # Path to cloud-init YAML file   block_device {     uuid                  = "<os_image_id>"     # Image ID for creating the boot volume     source_type           = "image"             # Source type (image)     destination_type      = "volume"            # Create a volume     volume_size           = <volume_size_in_gb> # Volume size in GB     volume_type           = "<volume_type_id>"  # Volume type ID     boot_index            = 0                   # Boot disk index     delete_on_termination = true                # Delete volume on instance termination   }   network {     uuid = "<network_id>"                      # Internal network ID to attach   } }
  1. Replace the placeholder values in angle brackets <...> with the actual values for your project, without brackets, for the following parameters:
    1. name — Unique instance name within your project.
    2. image_id — Identifier of the OS image. You can obtain it via the Horizon control panel ("Compute" > "Images" in the "ID/Name" column) or via CLI:
      		
      openstack image list
    3. flavor_id — Identifier of the instance flavor (defines CPU, RAM). You can obtain it  via Horizon ("Compute" > "Flavors" in the "ID/Name" column) or via CLI: 
      		
      openstack flavor list
    4. key_pair — Name of an existing SSH key pair. You can obtain it via Horizon ("Compute" > "Key Pairs" in "Name") or via CLI:
      		
      penstack keypair list

      If no key pair exists, create one following the instructions "Creating a Key Pair".
    5. user_data — Path to your user-data file (cloud-init or Ignition) for automatic instance configuration on first boot.

      Important: Always use user-data for automated setup — at minimum to create a user, assign access rights, and set a password. Without it, you may be unable to access the instance.

      Recommended
      : Use a relative path based on the variable "${path.module}" — it returns the path to the directory where the current .tf file is located. This makes the configuration portable and reliable in different environments, e.g.:
      		
      user_data = file("${path.module}/cloud-init.yaml")

      Not recommended: Specifying an absolute path, as it is tightly coupled to a specific system and may not work when transferring the configuration, e.g.:
      		
      # Linux/macOS user_data = file("/home/user/project/cloud-init.yaml") # Windows user_data = file("C:/Users/admin/project/cloud-init.yaml")

      Note: On Windows, prefer forward slashes (/). Backslashes (\) require double escaping (\\) and may cause errors. See the documentation sections "Cloud-Init" and "Ignition" for creating these files.
    6. uuid (in "block_device") — Must match the "image_id" value.
    7. volume_size — Size of the boot volume in GB (integer, e.g., 30).
    8. volume_type — Disk type ID:
      b22f9ad4-8069-42a8-bec3-5d27e146bb07 Hybrid-Datastore.
      97616de1-5db5-4bb4-809a-377a4b069c5d — All-Flash-Datastore.

      You can access the available volume types via the CLI:
      		
      openstack volume type list
    9. uuid (network) — uuid (in network) — Internal network ID; obtain via Horizon ("Network" > "Networks" > "Current Project Networks" in "ID/Name") or via CLI:
      		
      openstack network list --internal

"tf_instance_1" is the name of the instance within the Terraform configuration, and you can change it arbitrarily. However, all references to the instance must match, for example, when creating an instance, attaching an additional disk, or assigning a floating IP address.

Note: The fields "security_groups", "source_type", "destination_type", "boot_index", and "delete_on_termination" typically do not require changes.

If an instance needs an additional non-boot disk, duplicate the "block_device" block, set source_type = "blank", and assign a positive value to "boot_index" (for example, 1) — this indicates that the disk will not participate in booting.

After insertion of real values, the block may look like this:

		
# Resource block for creating a single instance in OpenStack via Terraform resource "openstack_compute_instance_v2" "tf_instance_1" {   name            = "test_instance"                                     # Name of the instance to create   image_id        = "os_image_id45fb7597-50db-4a39-b0f2-b6ce2eca773a"   # Identifier of the OS image to use   flavor_id       = "07050101-11d1-4916-a31d-153405676deb"              # Instance flavor (defines CPU, RAM)   key_pair        = "test_key_pair"                                     # Name of the SSH key pair   security_groups = ["default"]                                         # List of security groups applied to the instance   # Attach the user-data file (cloud-init)   user_data = file("${path.module}/cloud-init.yaml")                    # Path to the cloud-init YAML file   # Block to configure the primary (boot) disk of the instance   block_device {     uuid                  = "45fb7597-50db-4a39-b0f2-b6ce2eca773a"      # Image ID used to create the volume     source_type           = "image"                                     # Source type for volume creation (image)     destination_type      = "volume"                                    # Destination type (create a volume)     volume_size           = 30                                          # Size of the volume to create in gigabytes     volume_type           = "97616de1-5db5-4bb4-809a-377a4b069c5d"      # Volume type ID     boot_index            = 0                                           # Boot order (0 = primary boot disk)     delete_on_termination = true                                        # Delete volume when the instance is destroyed  }   # Block to configure the network connection of the instance   network {     uuid = "7c63ba21-958b-4856-b1da-764ed077b4df"                       # Network ID to attach the instance to   } }

  1. Save the changes to your configuration file.

Creating Multiple Instances

You can create a group of multiple instances at once, with identical or varying characteristics. The main difference from creating a single instance is the presence of the "count"  parameter in the resource block, which specifies the number of instances to create. Instance parameters can be set either as a single value applied to all instances in the group, or as lists in a separate "locals"  block for individual configuration of each instance. It is also possible to use dynamic parameter values — for example, appending a sequential index to each instance’s name via the "name" parameter, as shown in the example below.

When creating multiple instances, the Terraform resource name (for example, "tf_instances" in the example below) serves as the group name. You will use this group name later when attaching additional volumes or floating IP addresses.

  1. Add the following to your configuration:
    		
    # Local variables for multiple instance parameters locals {   image_ids       = ["<image-id-1>", "<image-id-2>", …, "<image-id-n>"] # List of OS image IDs   volume_sizes    = [<vol-size-1>, <vol-size-2>, …, <vol-size-n>]       # Corresponding disk sizes (GB)   flavor_ids      = ["<flavor-id-1>", …, "<flavor-id-n>"]             # List of flavor IDs   volume_types    = ["<vol-type-id-1>", …, "<vol-type-id-n>"]          # List of disk type IDs   key_pair        = "<my-single-keypair>"                              # SSH key pair name for all   user_data_files = ["${path.module}/<cloud-init-1.yaml>", …, "${path.module}/<cloud-init-n.yaml>"] # Paths to per-instance user-data files } # Resource block for multiple instancesresource "openstack_compute_instance_v2" "tf_instances" {   count           = length(local.image_ids)   name            = "instance-${count.index + 1}"   image_id        = local.image_ids[count.index]   flavor_id       = local.flavor_ids[count.index]   key_pair        = local.key_pair  user_data       = file(local.user_data_files[count.index])   security_groups = ["default"]   block_device {     uuid                  = local.image_ids[count.index]     source_type           = "image"     destination_type      = "volume"     volume_size           = local.volume_sizes[count.index]     volume_type           = local.volume_types[count.index]     boot_index            = 0     delete_on_termination = true  }   network {     uuid = "<network_id>"   } }
  1. Replace the placeholder values in angle brackets <...> with the actual values for your project, without brackets, for the following parameters:
    1. locals – the block of local variables. Here you specify either lists of parameters or single values to be used for all instances. You reference an element of a list via locals.<parameter_name>[<index>], for example:
      		
      flavor_id = local.flavor_ids[count.index] # for a list key_pair  = local.key_pair                # for a single value
    2. count – the number of instances to create. You may set it to a literal integer or compute it from the length of a parameter list, for example:
      		
      count = length(local.image_ids)  # Set count to the number of entries in local.image_ids, creating one instance per image ID count = 3                        # Create exactly three instances
    3. name – the unique name of each instance. 
      It may be defined dynamically, e.g.:
      		
      name = "My-instance-${count.index + 1}

      Or provided as a list in the “locals” block.
    4. image_id – the OS image identifier, available in the Horizon dashboard ("Compute" > "Images" in the "ID/Name" column) or via CLI:
      		
      openstack image list
    5. flavor_id – the resource configuration identifier (CPU, RAM), available in Horizon ("Compute" > "Flavors" in the "ID/Name" column) or via CLI:
      		
      openstack flavor list
    6. key_pair – the name of an existing SSH key pair, available in Horizon ("Compute" > "Key Pairs" in the "Name" column) or via CLI:
      		
      openstack keypair list
    7. user_data – the path to your user-data file (cloud-init or Ignition), which Terraform uses to bootstrap the instance on first launch. If you require per-instance customization, you can supply a list of file paths in the "locals" block. For more details on specifying the user-data path, see the "Creating a Single Instance" section above.
    8. uuid (block_device) – must match the “image_id”.
    9. volume_size – the disk size in gigabytes; may be given as a single integer or as a list in “locals”.
    10. volume_type – the disk type ID. For example:
      b22f9ad4-8069-42a8-bec3-5d27e146bb07 Hybrid-Datastore.
      97616de1-5db5-4bb4-809a-377a4b069c5d — All-Flash-Datastore.

      Retrieve available disk type IDs via CLI:
      		
      openstack volume type list
    11. uuid (network) uuid (network) – the internal network identifier, available in Horizon ("Network" > "Networks" > "Current Project Networks" in the "ID/Name" column) or via CLI:
      		
      openstack network list --internal

The fields "security_groups", "source_type", "destination_type", "boot_index" and "delete_on_termination" can also be set either as single values or as lists for each instance in the group.

When you have applied all changes, this block might look, for example, like this:

		
# Local variables block locals {   image_ids       = ["5890aa4d-05a2-4a58-a23b-bd7affc72770", "45fb7597-50db-4a39-b0f2-b6ce2eca773a", "5890aa4d-05a2-4a58-a23b-bd7affc72770"]  # OS image IDs   volume_sizes    = [30, 40, 30]                                                                                                         # Disk sizes in GB   flavor_ids      = ["07050101-11d1-4916-a31d-153405676deb", "2dadfd90-dd24-411b-b4ae-94da79404886", "07050101-11d1-4916-a31d-153405676deb"]  # Flavor IDs (instance types)   volume_types    = ["b22f9ad4-8069-42a8-bec3-5d27e146bb07", "97616de1-5db5-4bb4-809a-377a4b069c5d", "b22f9ad4-8069-42a8-bec3-5d27e146bb07"]  # Disk types   key_pair        = "test-keypair"                                                                                                       # Insert your SSH key pair name   user_data_files = [                                                                                                                     # Paths to cloud-init files     "${path.module}/cloud-init-1.yaml",     "${path.module}/cloud-init-2.yaml",     "${path.module}/cloud-init-3.yaml"   ] } # Resource block for multiple instances resource "openstack_compute_instance_v2" "tf_instances" {   count           = length(local.image_ids)                          # Number of instances equals the number of image IDs in the locals list   name            = "My-instance-${count.index + 1}"                 # Automatically assign a numbered instance name   image_id        = local.image_ids[count.index]                     # Image ID from the list   flavor_id       = local.flavor_ids[count.index]                    # Flavor ID from the list   key_pair        = local.key_pair                                   # SSH key pair (same for all instances)   user_data       = file(local.user_data_files[count.index])         # Per-instance cloud-init file   security_groups = ["default"]                                      # Security group (same for all instances)   # Boot disk block   block_device {     uuid                  = local.image_ids[count.index]             # OS image ID (must match image_id)     source_type           = "image"                                  # Source type for volume creation     destination_type      = "volume"                                 # Destination type (create a volume)     volume_size           = local.volume_sizes[count.index]          # Disk size in GB     volume_type           = local.volume_types[count.index]          # Disk type ID     boot_index            = 0                                        # Boot disk index (0 = primary boot disk)     delete_on_termination = true                                     # Delete volume when the instance is destroyed   }   # Network configuration   network {     uuid = "7c63ba21-958b-4856-b1da-764ed077b4df"                     # Internal project network ID   } }

  1. Save the changes to your configuration file.

II. Allocating a Floating IP to an Instance

When an instance is created in OpenStack, it is automatically assigned an internal IP address for communication within the cloud infrastructure. However, to access the instance from the Internet, an external (floating) IP address must be allocated.

Although OpenStack maintains a pool of external IP addresses, no floating IP is allocated by default in a new project. In this guide, we will examine the process of allocating floating IP addresses from the shared pool and assigning them to individual instances or to groups of instances, thus enabling external network access to your virtual machines.

We will base our work on the configuration files from Section I, “Creating an Instance via Terraform”, assuming that our additions complement those files.

Allocating an IP from the Pool and Assigning It to an Instance

  1. Add the following resource blocks to your Terraform configuration file:
    		
    # Allocate a floating IP from the cloud infrastructure’s external network pool resource "openstack_networking_floatingip_v2" "float_ip" {   pool = "<External_IP_pool_name>"  # Specify the name of the external network from which the floating IP will be allocated } # Associate the allocated floating IP with the created instance resource "openstack_compute_floatingip_associate_v2" "associate_float_ip" {   floating_ip = openstack_networking_floatingip_v2.float_ip.address   instance_id = openstack_compute_instance_v2.tf_instance_1.id }
  1. Specify the parameter pool — the name of the external network whose pool will supply the floating IP. You can find external network names in Horizon ("Network" > "Networks" > "External Networks", check the "ID/Name" column) or via the CLI:
    		
    openstack network list --external

    If the name of your instance resource differs from "tf_instance_1", replace it in the "instance_id" line.

    After substitution, the block might look like this:
    		
    # Allocate a floating IP from the cloud infrastructure’s external network pool resource "openstack_networking_floatingip_v2" "float_ip" {   pool = "Floating_IP59"  # Name of the external network pool } # Associate the allocated floating IP with the created instance resource "openstack_compute_floatingip_associate_v2" "associate_float_ip" {   floating_ip = openstack_networking_floatingip_v2.float_ip.address   instance_id = openstack_compute_instance_v2.tf_instance_1.id }
  1. Save your changes.

Assigning a Previously Allocated Floating IP

  1. To assign an already allocated floating IP to an instance, add the following block to your configuration file:
    		
    resource "openstack_compute_floatingip_associate_v2" "associate_float_ip" {   floating_ip = "<Allocated_IP>"    # Enter the floating IP already drawn from the pool   instance_id = "<Instance_ID>"     # Enter the instance’s ID or a reference to it in your Terraform configuration }
  1. Specify your values for:
    1. floating_ip — the IPv4 address allocated from the external network pool and not attached to any instance. You can view available ("DOWN") floating IPs in Horizon ("Network" > "Floating IPs", check the "ID/Floating IP" column) or via CLI:
      		
      openstack floating ip list --status DOWN
    2. instance_id — the ID of your project’s instance or a Terraform reference to it. You can find instance IDs in Horizon ("Compute" > "Instances", check the "ID/Name" column) or via CLI:
      		
      openstack server list

For example, the block for adding a floating IP address to the instance "tf_instance_1" from the "Creating a Single Instance" example in Section I would look like this:

		
resource "openstack_compute_floatingip_associate_v2" "associate_float_ip" {   floating_ip = "198.51.100.101"              # Already allocated floating IP   instance_id = openstack_compute_instance_v2.tf_instance_1.id  # Terraform reference }

In this case, a reference to the instance’s name in the Terraform configuration is used.

Or, specifying the instance ID directly:
		
resource "openstack_compute_floatingip_associate_v2" "associate_float_ip" {   floating_ip = "198.51.100.101"    # Already allocated floating IP   instance_id = "ea6ddfe0-fadf-4c96-aa64-77a61b639112"  # Instance ID }

  1. Save your configuration file.

Allocating Multiple Floating IPs and Assigning Them to a Group of Instances

It is also possible to allocate floating IP addresses to a group of instances, and here we will look at how to do this. We will allocate floating IP addresses from an external address pool and assign them to instances in the group. We will base our configuration on the configuration used to create the "tf_instances" group, described in Section I, "Creating an instance via Terraform: Creating Multiple instances".

  1. Add the following blocks:
    		
    # Allocate floating IPs from the external network pool for each instance resource "openstack_networking_floatingip_v2" "fip" {   count = length(openstack_compute_instance_v2.tf_instances)   pool  = "<external_network_name>"  # Specify the external network to act as the floating IP pool } # Associate the allocated floating IPs with each instanceresource "openstack_compute_floatingip_associate_v2" "fip_associate" {   count       = length(openstack_compute_instance_v2.tf_instances)   floating_ip = openstack_networking_floatingip_v2.fip[count.index].address   instance_id = openstack_compute_instance_v2.tf_instances[count.index].id }
  1. Specify the pool parameter with the name of your external network. You can list external networks via Horizon (“Network” > “Networks” > “External Networks” check the "ID/Name" column) or via CLI:
    		
    openstack network list --external

    If your instance group resource is named differently from "tf_instances", replace it in the "instance_id" line.

    After specifying the external network name, the block may look like this:
    		
    # Allocate floating IP addresses from the pool for each instance resource "openstack_networking_floatingip_v2" "fip" {   count = length(openstack_compute_instance_v2.tf_instances)   pool  = "Floating_IP59" # Name of the external network (floating IP pool) } # Associate the allocated floating IP addresses with the instances resource "openstack_compute_floatingip_associate_v2" "fip_associate" {   count       = length(openstack_compute_instance_v2.tf_instances)   floating_ip = openstack_networking_floatingip_v2.fip[count.index].address   instance_id = openstack_compute_instance_v2.tf_instances[count.index].id }
  1. Save your changes.

III. Attaching an Additional Disk to an Instance

You can attach an additional disk to an instance—either a new one or an existing one.

You can attach a disk at instance creation by duplicating the block_device configuration block. If you need an empty additional disk, set boot_index = -1 (the disk will not participate in booting) and source_type = "blank".

If you need to add a disk after the instance has already been created, we’ll show you how to do this by extending the configurations from Section I.

Attaching a New Disk to a Newly Created Instance

  1. In the configuration file from Section I, "Creating an Instance via Terraform: Creating a Single Instance", add the blocks to create and attach the new volume:
    		
    # Create an additional volumeresource "openstack_blockstorage_volume_v3" "additional_volume" {   name        = "<volume_name>"    # Specify the name for the new volume   size        = <volume_size>      # Specify the size for the new volume (GB)   volume_type = "<volume_type>"    # Specify the type for the new volume } # Attach the newly created additional volume to the instance resource "openstack_compute_volume_attach_v2" "volume_attachment" {   instance_id = openstack_compute_instance_v2.tf_instance_1.id   volume_id   = openstack_blockstorage_volume_v3.additional_volume.id }
  2. Replace the placeholder values in angle brackets <...> with the actual values for your project, without brackets, for the following parameters:
  1. volume_name — an arbitrary, unique name for the volume within your project (e.g. my-additional-volume-01).
  2. volume_size — the size of the new volume in gigabytes (integer, e.g. 20).
  3. volume_type — the ID of the volume type:
    • b22f9ad4-8069-42a8-bec3-5d27e146bb07 Hybrid-Datastore.
    • 97616de1-5db5-4bb4-809a-377a4b069c5d — All-Flash-Datastore.

You can list available volume types via CLI:

		
openstack volume type list

If your instance resource name is not "tf_instance_1", replace it in the "instance_id" line.

After updating, the block might look like:

		
# Create an additional volumeresource "openstack_blockstorage_volume_v3" "additional_volume" {   name        = "my-additional-volume"        # Volume name   size        = 20                              # Volume size (GB)   volume_type = "b22f9ad4-8069-42a8-bec3-5d27e146bb07"  # Volume type ID } # Attach the additional volume to the instance resource "openstack_compute_volume_attach_v2" "volume_attachment" {   instance_id = openstack_compute_instance_v2.tf_instance_1.id   volume_id   = openstack_blockstorage_volume_v3.additional_volume.id }

  1. Save your changes.

Attaching an Existing Disk to an Instance

  1. In your configuration file, add the block to attach an existing volume as an additional disk:
    		
    # Attach an existing volume to an instance as an additional disk resource "openstack_compute_volume_attach_v2" "existing_volume_attachment" {   instance_id = "<instance_id>"        # Specify the instance ID   volume_id   = "<existing_volume_id>" # Specify the existing volume ID }
  2. Replace the placeholder values in angle brackets <...> with the actual values for your project, without brackets, for the following parameters:
    1. instance_id — The ID of your project’s instance or a Terraform reference to it. You can find instance IDs via Horizon ("Compute" > "Instances", check the "ID/Name" column) or via CLI:
      		
      openstack server list
    2. volume_id — the ID of the existing volume you wish to attach. List available volumes with status "available" via Horizon ("Storage" > "Volumes", filter by status "Available") or CLI:
      		
      openstack volume list --status available

For example, attaching an existing volume to the "tf_instance_1" instance from the “Creating a Single Instance” example in Section I might look like:

		
# Attach an existing volume as an additional disk resource "openstack_compute_volume_attach_v2" "existing_volume_attachment" {   instance_id = openstack_compute_instance_v2.tf_instance_1.id # Terraform reference   volume_id   = "9874221b-018d-40ef-a717-183cdc6e30d3"         # Existing volume ID }

Or, specifying the instance ID directly:

		
# Attach an existing volume to an existing instance as an additional disk resource "openstack_compute_volume_attach_v2" "existing_volume_attachment" {   instance_id = "ea6ddfe0-fadf-4c96-aa64-77a61b639114"  # Instance ID   volume_id   = "9874221b-018d-40ef-a717-183cdc6e30d3"  # Existing volume ID }

  1. Save your configuration file.

Connecting multiple new disks to a group of instances

  1. Add the following resource blocks to the configuration file from Section I, "Creating an Instance vis Terraform: Creating Multiple Instances", to create volumes and attach them to the new instances:
    		
    # Block creating additional volumes for each instance resource "openstack_blockstorage_volume_v3" "extra_volumes" {   count        = length(openstack_compute_instance_v2.tf_instances)            # Number of additional volumes equals number of instances   name         = local.extra_volume_names[count.index]                         # Volume name from the list   size         = local.extra_volume_sizes[count.index]                         # Volume size from the list   volume_type  = local.extra_volume_types[count.index]                         # Volume type from the list } # Block attaching additional volumes to corresponding instances resource "openstack_compute_volume_attach_v2" "extra_attachments" {   count       = length(openstack_compute_instance_v2.tf_instances)            # Number of attachments equals number of instances   instance_id = openstack_compute_instance_v2.tf_instances[count.index].id    # ID of the corresponding instance   volume_id   = openstack_blockstorage_volume_v3.extra_volumes[count.index].id # ID of the corresponding volume }

    Note: Insert these blocks after the main resource "openstack_compute_instance_v2" block but before any "output" or "network" sections.

    Extend the "locals" block with parameters for the additional volumes:
    		
    # Local variables for additional volumes – specify your own values locals {   extra_volume_names = ["<extra-volume-name-1>", "<extra-volume-name-2>", …, "<extra-volume-name-n>"] # Names of additional volumes   extra_volume_sizes = [<extra-size-1>, <extra-size-2>,…, <extra-size-n>]              # Sizes of additional volumes in GB (integer)   extra_volume_types = ["<extra-volume-type-1>", "<extra-volume-type-2>",…,"<extra-volume-type-n>"] # IDs of volume types }

    Note: If you require unique values for each volume (e.g. different names or sizes), list them in  "locals" and reference them in the resources. If all parameters are identical (e.g. same size) or can be generated dynamically (e.g. name with index), you may omit them from locals and specify directly in the resource.
  2. Specify your own "locals" in the locals block:
    1. extra_volume_names — arbitrary, unique names for the volumes.
    2. extra_volume_sizes — sizes in GB as integers (e.g. 20).
    3. extra_volume_types — IDs of the volume types:
      b22f9ad4-8069-42a8-bec3-5d27e146bb07 — Hybrid-Datastore.
      97616de1-5db5-4bb4-809a-377a4b069c5d — All-Flash-Datastore.

      You can list available volume types via CLI:
      		
      openstack volume type lis

If your instance group resource is not named "tf_instances", replace it in the "instance_id" line.

When all parameters are defined individually, the blocks remain unchanged, and the "locals" block might look like:

		
# Additional settings in locals locals {   # Parameters for additional volumes   extra_volume_names = ["extra-disk-1", "extra-disk-2", "extra-disk-3"]   extra_volume_sizes = [10, 20, 15]   extra_volume_types = ["b22f9ad4-8069-42a8-bec3-5d27e146bb07", "97616de1-5db5-4bb4-809a-377a4b069c5d", "b22f9ad4-8069-42a8-bec3-5d27e146bb07"] }

If, for example, you specify a single size for all volumes and generate names dynamically, the creation block becomes:

		
# Create additional volumes for each instance resource "openstack_blockstorage_volume_v3" "extra_volumes" {   count        = length(openstack_compute_instance_v2.tf_instances)       # Number of volumes equals number of instances   name         = "extra-volume-${count.index + 1}"                       # Automatic volume name with index   size         = 10                                                        # Single size for all volumes (GB)   volume_type  = local.extra_volume_types[count.index]                     # Volume type – individual per volume }

The attachment block remains unchanged:

		
# Attach additional volumes to instances resource "openstack_compute_volume_attach_v2" "extra_attachments" {   count       = length(openstack_compute_instance_v2.tf_instances)   instance_id = openstack_compute_instance_v2.tf_instances[count.index].id   volume_id   = openstack_blockstorage_volume_v3.extra_volumes[count.index].id }

In this case, specify only the volume types in "locals":

		
# Additional settings in localslocals {   extra_volume_types = ["b22f9ad4-8069-42a8-bec3-5d27e146bb07", "97616de1-5db5-4bb4-809a-377a4b069c5d", "b22f9ad4-8069-42a8-bec3-5d27e146bb07"] # Types of additional volumes }

  1. Save your changes.

IV. Outputting Information on Created Resources: Using the "output" Block

After creating elements of the cloud infrastructure, it can be useful to obtain information about the identifiers of the resources you have created—instances, volumes, floating IP addresses, and so forth. Terraform provides a special "output" block that displays the required data in the terminal once operations have completed. The "output" block also allows these parameters to be used in CI/CD pipelines, external scripts, and for access from other modules.

"output" blocks are added at the end of the configuration file and do not affect the infrastructure itself.

Structure of the output Block

The output block has the following structure:

		
output "<output_name>" {   value       = <value_or_reference>   description = "<optional_description>"   sensitive   = <true|false>  # hides the value in the terminal }

Where:

  • output "<output_name>" — an arbitrary name under which the result will be shown. You can reference this name to access the output from other modules.
  • value — the value or reference to a parameter. How to form this is described below.
  • description — an optional explanation of the output parameter.
  • sensitive — an optional flag. If set to "true", the value will not appear in the terminal when running "terraform apply", but it remains accessible programmatically (including from other modules). Defaults to "false", and is typically omitted.

Forming the “value” Parameter

The "value" parameter is constructed as follows:

		
value = <resource_type>.<resource_name>.<attribute>

Where:

  • <resource_type>— the Terraform resource type. Available resource types are listed below.
  • <resource_name>— the name you assigned in the "resource" block.
  • <attribute>— the attribute to output, e.g.  "id", "address", "name", etc.

If you do not need to pass the value to another module but wish to display it with explanatory text in the terminal, format "value" like this:

		
value = "Instance ID: ${<resource_type>.<resource_name>.<attribute>}"

For example:

		
value = "Instance ID: ${openstack_compute_instance_v2.tf_instance_1.id}"

Main OpenStack Resource Types in Terraform

  • openstack_compute_instance_v2 — virtual machine (instance);
  • openstack_blockstorage_volume_v3 — additional disk (volume);
  • openstack_compute_volume_attach_v2 — attaching a volume to an instance;
  • openstack_networking_floatingip_v2 — allocating an external IP address;
  • openstack_compute_floatingip_associate_v2 — assigning a floating IP to an instance;
  • openstack_networking_port_v2 — creating a network port;
  • openstack_keypair_v2 — importing or generating an SSH key;
  • openstack_security_group_v2 — creating a security group.

For a full list of supported resources, refer to the official OpenStack Terraform provider documentation.

Cross‑Module Usage of "output"

When using a modular structure, "output" blocks enable you to pass values from one module to another. This is helpful when logic is split: one module might create infrastructure, and another might consume those resources for configuration, monitoring, or connection.

For example, a module may return an IP address, password, or resource identifier. A calling module can then reference it using:

		
module.<module_name>.<output_block_name>

Where:

  • module — Terraform keyword indicating module access;
  • <module_name> — the name assigned to the module in your configuration;
  • <output_block_name> — the name given to the output parameter in the module via its "output" block.

Example:

		
module.vm_setup.instance_ip

This returns the IP address from the output "instance_ip" block defined inside the "vm_setup" module, which might look like:

		
# Inside the vm_setup module output "instance_ip" {   value = openstack_networking_floatingip_v2.fip.address }

Examples of “output” Block Declarations

Single instance

  • Configuration block:
    		
    output "instance_id" {   value       = openstack_compute_instance_v2.tf_instance_1.id   description = "ID of the created instance" }
  • Output example:
    		
    instance_id = "ea6ddfe0-fadf-4c96-aa64-77a61b639112"

Group of instances:

  • Configuration block:
    		
    output "instance_ids" {   value       = [for instance in openstack_compute_instance_v2.tf_instances : instance.id]   description = "List of IDs for all created instances" }
  • Output example:
    		
    instance_ids = [  "f01fa473-e7d5-4a2e-bf8e-45e4bd1aa622",  "e24e2875-8fb1-45d7-9d53-1fd94d89f02c"]

Single floating IP:

  • Configuration block:
    		
    output "floating_ip" {   value       = openstack_networking_floatingip_v2.float_ip.address   description = "Floating IP address assigned to the instance" }
  • Output example:
    		
    floating_ip = "198.51.100.101"

Group of floating IPs:

  • Configuration block:
    		
    output "floating_ips" {   value       = [for fip in openstack_networking_floatingip_v2.fip : fip.address]   description = "List of all assigned floating IP addresses" }
  • Output example:
    		
    floating_ips = [  "198.51.100.201",  "198.51.100.202"]

Single additional disk:

  • Configuration block:
    		
    output "volume_id" {   value       = openstack_blockstorage_volume_v3.additional_volume.id   description = "ID дополнительного диска" }
  • Output example:
    		
    volume_id = "4f6c2340-63d4-4b64-84c3-3aa3f8b0cf12"

Multiple additional disks:

  • Configuration block:
    		
    output "volume_ids" {   value       = [for vol in openstack_blockstorage_volume_v3.extra_volumes : vol.id]   description = "List of IDs for all created additional volumes" }
  • Output example:
    		
    volume_ids = [  "b90a4304-90c8-42b3-8c84-0c5a2f0aa3d3",  "3f2c8896-1e72-4a38-8053-d3e7e6c4bbff"]

V. Applying Changes to the Infrastructure

Once your configuration file is ready, apply it to the cloud infrastructure by following these steps:

  1. Open a terminal:
    • Windows: PowerShell or Command Prompt (CMD).
    • macOS/Linux: Bash, Zsh, or another terminal emulator.
  2. Change to the directory containing your configuration file:
    • macOS/Linux, for example:
      		
      cd /home/user/terraform/conf
    • Windows (PowerShell or CMD), for example:
      		
      cd "C:\terraform\conf
  3. Initialise Terraform (if you have not already done so):
    		
    terraform init

    Upon successful initialisation, you will see:
    		
    Terraform has been successfully initialized!
  4. Validate the configuration files to check for errors:
    		
    terraform validate

    If validation succeeds, you will see:
    		
    Success! The configuration is valid.
    If any errors are detected, correct them before proceeding.
  5. Format all .tf files in the current directory according to HashiCorp’s recommendations:
    		
    terraform fmt
    This improves code readability and ensures consistency.
  6. Display and review the proposed changes to your infrastructure:
    		
    terraform plan
    If the planned changes do not match your expectations, adjust your configuration and run terraform plan again.
  7. Apply the changes to the cloud infrastructure:
    		
    terraform apply
    Terraform will prompt for confirmation:
    		
    Do you want to perform these actions?   Terraform will perform the actions described above.   Only 'yes' will be accepted to approve.   Enter a value: 
  8. Type  "yes" and press "Enter" to confirm.
    After a successful apply, you will see a summary, for example:
    		
    Apply complete! Resources: 2 added, 0 changed, 0 destroyed.