Terraform lets you describe OpenStack cloud infrastructure as code and manage its lifecycle using a single set of commands. Instead of manually creating resources in the control panel, you can define a network, subnet, Security Group, virtual machine, and Floating IP in advance, and then apply the configuration with Terraform.
In this guide, we will configure the OpenStack Provider, show how to authenticate securely without exposing credentials, and walk through the main commands step by step:
terraform init
terraform plan
terraform apply
terraform output
terraform destroy
As a result, Terraform will create:
- A private network;
- A subnet;
- A Security Group with the required rules;
- A virtual machine;
- A Floating IP for external access.
We will also cover terraform.tfstate separately: the state file where Terraform stores information about the resources it has created and how they relate to the configuration. This file must not be carelessly published to Git or edited manually, because the state may contain sensitive data and is critical for proper infrastructure management.
At the end, we will remove the created resources with terraform destroy and review the consequences of this command: the virtual machine, network, Floating IP, and other Terraform-managed resources will be deleted from OpenStack, so you must carefully review the change plan before confirming destroy.
How Terraform Manages OpenStack Infrastructure
Terraform lets you describe infrastructure declaratively: the configuration specifies the desired state, and Terraform determines what actions need to be performed in OpenStack to bring the actual resources to that state.
Instead of performing a sequence of manual actions in the dashboard—creating a network, subnet, Security Group, virtual machine, and Floating IP—you can describe these resources in .tf files and then apply them with a single command.
What Is Infrastructure as Code
Infrastructure as Code, or IaC, is an approach in which infrastructure is described using configuration files.
For example, instead of creating a network manually, you can define it in Terraform in advance:
resource "openstack_networking_network_v2" "private" {
name = "terraform-network"
admin_state_up = true
}
You can describe the following in the same way:
- Subnets;
- Virtual machines;
- Security groups;
- Floating IPs;
- Additional network parameters.
The main advantage of this approach is repeatability. The same configuration can be reused in another project or environment with minimal changes.
Terraform also lets you preview the planned changes and reduces the number of manual operations in the cloud control panel.
How Terraform Interacts with OpenStack
Terraform does not create resources directly by itself. Providers are used to interact with a specific platform.
In our case, we will need the OpenStack Provider: terraform-provider-openstack/openstack
Terraform passes information from the configuration to the provider, which then sends the corresponding requests to the OpenStack API.
At a high level, the workflow looks like this:

Therefore, Terraform requires OpenStack authentication parameters and access to the relevant APIs.
When terraform plan is run, Terraform analyzes the configuration and compares it with the current state.
When terraform apply is run, the provider creates, modifies, or deletes resources through the OpenStack API.
Resources We Will Create in This Guide
In this example, we will create a minimal cloud infrastructure for a single virtual machine.
It will include:

We will also create a security group with rules for the required inbound connections.
The following resource types will be defined in Terraform:
openstack_networking_network_v2
openstack_networking_subnet_v2
openstack_networking_secgroup_v2
openstack_networking_secgroup_rule_v2
openstack_compute_instance_v2
openstack_networking_floatingip_v2
Depending on the specific OpenStack infrastructure, a separate association resource or port configuration may also be used to attach the Floating IP.
After terraform apply is run, Terraform should automatically create the dependencies between these objects in the correct order.
How Terraform Tracks Infrastructure State
Terraform needs to know which real OpenStack objects correspond to the resources defined in the configuration.
Terraform uses state for this.
By default, local state is stored in the file: terraform.tfstate
In this file, Terraform stores the identifiers of the resources it creates, along with other internal metadata.
For example, after a network is created, the state will contain the mapping between openstack_networking_network_v2.private and the actual network ID in OpenStack.
This allows Terraform, on the next run, to determine whether it needs to create a new object, modify an existing one, or leave it unchanged.
State is especially important for the following commands:
terraform plan
terraform apply
terraform destroy
If the state is lost or replaced with the wrong version, Terraform may no longer be able to correctly map the configuration to the existing infrastructure. Therefore, if the infrastructure is managed with Terraform, you should avoid making manual changes through the cloud management web interface, because the state will also become inconsistent.
For this reason, we will return to state storage separately after creating the resources.
Preparing the Terraform Environment

Before defining OpenStack resources, we will install Terraform, create a working directory, and set up a secure authentication method.
Installing Terraform
On Ubuntu, Terraform can be installed from HashiCorp’s official APT repository.
First, install the required packages:
sudo apt update
sudo apt install -y gnupg software-properties-common curl
Add the HashiCorp key:
wget -O- https://apt.releases.hashicorp.com/gpg | \
gpg –dearmor | \
sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg > /dev/null
Add the repository:
echo "deb [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
Update the package index and install Terraform:
sudo apt update
sudo apt install -y terraform
Check it: terraform version
The output should show the installed Terraform version.
Preparing the Project and Configuration Files
Create a separate project directory:
mkdir -p ~/terraform-openstack
cd ~/terraform-openstack
For convenience, split the configuration into several files:
providers.tf
network.tf
security.tf
instance.tf
outputs.tf
variables.tf
Terraform automatically reads all .tf files in the current working directory, so you do not need to include each one manually.
Create the initial files: touch providers.tf network.tf security.tf instance.tf outputs.tf variables.tf
Also create a .gitignore right away: nano .gitignore
Add the following:
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
crash.log
Terraform state and local variable files should not accidentally end up in the Git repository.
Obtaining OpenStack connection parameters
Terraform requires OpenStack parameters to authenticate.
Depending on the cloud provider, they may be provided in several formats:
- OpenRC file;
- Clouds.yaml;
- Application credentials;
- A separate set of environment variables.
A typical OpenRC contains variables such as:
OS_AUTH_URL
OS_USERNAME
OS_PASSWORD
OS_PROJECT_NAME
OS_USER_DOMAIN_NAME
OS_PROJECT_DOMAIN_NAME
OS_REGION_NAME
For example:
export OS_AUTH_URL=https://openstack.example.com:5000/v3
export OS_USERNAME=terraform-user
export OS_PASSWORD=…
export OS_PROJECT_NAME=project-name
export OS_USER_DOMAIN_NAME=Default
export OS_PROJECT_DOMAIN_NAME=Default
export OS_REGION_NAME=RegionOne
Real credentials should not be added directly to the Terraform configuration.
If the provider supplies a ready-to-use OpenRC file, it is usually enough to source it: source openrc.sh
The variables will then be available in the current shell session.
Authentication without storing credentials in Terraform code
In providers.tf, we will not specify a password, username, or other secrets.
The OpenStack provider can use the standard OS_* environment variables, so the configuration can remain free of credentials.
For example:
provider “openstack” {
}
The required parameters are then passed through the environment.
You can verify that the variables are loaded without displaying their values: env | grep ‘^OS_’ | sed ‘s/=.*$/=<set>/’
The output will look something like this:
OS_AUTH_URL=<set>
OS_USERNAME=<set>
OS_PASSWORD=<set>
OS_PROJECT_NAME=<set>
OS_REGION_NAME=<set>
This lets you confirm that the authentication parameters are present without exposing their actual values.
For application credentials, the approach is similar: the secret is passed through an environment variable rather than written directly to a .tf file.
Configuring the Terraform Provider for OpenStack
After preparing the environment, we will configure the OpenStack Provider and initialize the project for the first time.
Creating the providers.tf file
Open it with nano: nano providers.tf
Add the terraform block:
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 3.0"
}
}
}
provider "openstack" {
}
This tells Terraform which provider to download.
The block itself:
provider "openstack" {
}
remains empty because the authentication parameters will be taken from the environment.
Specifying the OpenStack Provider Version
The following constraint is used in required_providers: version = "~> 3.0"
It allows Terraform to use compatible releases from the specified major version line without automatically moving to the next major version, which may be incompatible.
After terraform init, the exact selected provider version will also be recorded in .terraform.lock.hcl
This file should generally be stored in Git, as it helps ensure that the same provider versions are used across different environments.
Initializing the working directory with terraform init
Now run: terraform init
Terraform:
- Reads providers.tf;
- Finds the OpenStack provider;
- Downloads the required version;
- Creates the .terraform directory;
- Creates or updates .terraform.lock.hcl.
If initialization completes successfully, the following message appears: Terraform has been successfully initialized!
The init command does not create any cloud resources. It only prepares the local Terraform project for further use.
Verifying the downloaded provider
You can view the providers for the current project with this command: terraform providers
The output should include: registry.terraform.io/terraform-provider-openstack/openstack
You can also check the contents of the lock file: grep -A 5 ‘terraform-provider-openstack/openstack’ .terraform.lock.hcl
After successful initialization, the environment is ready for defining the network, subnet, Security Group, virtual machine, and Floating IP.
Creating an OpenStack Network and Subnet

After configuring the provider, you can define the basic network infrastructure. For the virtual machine, we will create a separate private network and subnet through which it will receive an internal IP address.
This approach is more convenient than connecting the VM directly to an existing network chosen at random, because the entire structure becomes part of the Terraform configuration and is managed together with the other resources.
Defining a Private Network in Terraform
Open the file: nano network.tf
Add a network resource:
resource “openstack_networking_network_v2” “private” {
name = “terraform-network”
admin_state_up = true
}
The name = “terraform-network” parameter sets the network name in OpenStack.
The admin_state_up = true parameter means that the network should be enabled after it is created.
Terraform will refer to this network within the project by its logical name: openstack_networking_network_v2.private
This name is used only in the Terraform configuration and does not have to match the resource name in OpenStack.
Creating a Subnet and CIDR
Now add a subnet:
resource "openstack_networking_subnet_v2" "private" {
name = "terraform-subnet"
network_id = openstack_networking_network_v2.private.id
cidr = "192.168.50.0/24"
ip_version = 4
}
The key relationship here is: network_id = openstack_networking_network_v2.private.id
Terraform retrieves the network ID after creating the network and uses it when creating the subnet.
This example uses the following range: 192.168.50.0/24
This range provides addresses within the private network.
The virtual machine will later be assigned one of these addresses, for example: 192.168.50.10
The specific IP address is usually assigned automatically by OpenStack.
Configuring DNS and Network Settings
If required, you can also specify DNS servers for the subnet.
For example:
resource "openstack_networking_subnet_v2" "private" {
name = "terraform-subnet"
network_id = openstack_networking_network_v2.private.id
cidr = "192.168.50.0/24"
ip_version = 4
dns_nameservers = [
"1.1.1.1",
"8.8.8.8"
]
}
In a real project, internal resolver addresses in the cloud infrastructure may be used instead of public DNS servers.
If required, you can also explicitly configure DHCP, the gateway, and the allocation pool. For example: enable_dhcp = true
However, if no additional settings are required, OpenStack can use the default values.
At this stage, the configuration contains a private network and a subnet, which will later be used by the virtual machine.
Creating a Security Group
The next step is to define access rules for the VM.
A Security Group in OpenStack acts as a set of network rules that allow or block specific inbound and outbound traffic.
For this example, we will create a separate group and allow SSH, ICMP, and HTTP/HTTPS.
Security group description
Open: nano security.tf
Create a security group:
resource "openstack_networking_secgroup_v2" "vm" {
name = "terraform-vm-sg"
description = "Security group for Terraform VM"
}
We now have a separate group: terraform-vm-sg
Access rules are added as separate resources.
Allowing SSH Access
Add a rule for TCP port 22:
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.vm.id
}
The direction = "ingress" parameter specifies inbound traffic.
A range of 22 → 22 allows only SSH.
This demo example uses: 0.0.0.0/0
that is, connections are allowed from any IPv4 address.
For real infrastructure, it is safer to restrict SSH to a specific administrative IP address or subnet, for example: 203.0.113.25/32
Adding Rules for ICMP or Web Traffic
To test whether the VM is reachable, you can allow ICMP:
resource "openstack_networking_secgroup_rule_v2" "icmp" {
direction = "ingress"
ethertype = "IPv4"
protocol = "icmp"
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.vm.id
}
If the server will be used as a web server, add HTTP:
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.vm.id
}
And HTTPS:
resource "openstack_networking_secgroup_rule_v2" "https" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 443
port_range_max = 443
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.vm.id
}
As a result, the security group will allow only the necessary types of inbound traffic.
Why you should not open unnecessary ports
A Security Group should be designed according to the principle of least privilege.
If a VM uses only SSH and HTTPS, there is no reason to allow the following in advance:
3306
5432
6379
8080
9000
or other service ports to the entire internet.
Every additional rule increases the attack surface.
Therefore, in a real-world project, you should:
- Allow only the ports that are actually in use;
- Restrict administrative access to trusted IP addresses;
- Avoid exposing databases directly;
- Regularly review the list of rules.
Terraform makes this review easier because the entire Security Group is defined in code, and changes are clearly visible through terraform plan.
Creating a virtual machine

You can now define the compute instance.
The VM will be connected to the created network, assigned a Security Group, and later given a Floating IP for external access.
Selecting an image and flavor
Before creating a VM, you need to determine which images and flavors are available in the specific OpenStack project.
An image defines the operating system, for example: Ubuntu 24.04
A flavor specifies the compute resources:
vCPU
RAM
Disk
You can use names in the configuration:
image_name = "Ubuntu 24.04"
flavor_name = "standard-1"
However, the actual names depend on the cloud provider.
Therefore, before running Terraform, you need to check them in the OpenStack dashboard or via the CLI.
For example, openstack image list and openstack flavor list.
Connecting a VM to the created network
Open: nano instance.tf
Create a virtual machine:
resource "openstack_compute_instance_v2" "vm" {
name = "terraform-vm"
image_name = "Ubuntu 24.04"
flavor_name = "standard-1"
network {
uuid = openstack_networking_network_v2.private.id
}
}
Here, uuid = openstack_networking_network_v2.private.id associates the VM with the network that Terraform creates in the same project.
When terraform apply is run, Terraform determines that it must create the network first and then pass its ID to the VM configuration.
Attaching an SSH key and a security group
For SSH access, it is best to use a previously created OpenStack key pair.
For example: key_pair = "terraform-key"
We will also add a security group:
security_groups = [
openstack_networking_secgroup_v2.vm.name
]
Resulting resource:
resource "openstack_compute_instance_v2" "vm" {
name = "terraform-vm"
image_name = "Ubuntu 24.04"
flavor_name = "standard-1"
key_pair = "terraform-key"
security_groups = [
openstack_networking_secgroup_v2.vm.name
]
network {
uuid = openstack_networking_network_v2.private.id
}
}
The VM will now be created in the target network and immediately assigned the specified access rules.
If the key pair does not yet exist in OpenStack, you can either create it in advance or define it as a separate Terraform resource.
Dependencies between Terraform resources
Terraform automatically builds a dependency graph based on references between resources.
For example, network_id = openstack_networking_network_v2.private.id creates a dependency: Network → Subnet
Similarly, uuid = openstack_networking_network_v2.private.id creates a dependency: Network → VM
Similarly, a Security Group must exist before it can be assigned to a virtual machine.
A simplified chain looks like this:

Terraform determines the resource creation order automatically and usually does not require a manually specified depends_on.
An explicit depends_on is needed only when a dependency exists logically but is not expressed through references between arguments.
After adding the network, subnet, Security Group, and VM, the configuration already describes the core part of the infrastructure. Next, you can create a Floating IP and associate it with the virtual machine.
Attaching a Floating IP
A private network allows a virtual machine to communicate with other resources within OpenStack, but a public address is required to connect to it from the internet. In OpenStack, this role is typically handled by a Floating IP.
Terraform can automatically allocate an address from an external network and associate it with the created VM.
Creating a Floating IP from an external network
A Floating IP is created from an OpenStack external address pool. The name of the external network depends on the specific cloud provider and may look like this, for example:
public
external
ext-net
Before using it, it is a good idea to check the available networks: openstack network list –external
In Terraform, we will create the Floating IP as a separate resource. Add the following to instance.tf:
resource "openstack_networking_floatingip_v2" "vm" {
pool = "public"
}
Replace the pool = "public" parameter with the actual name of the external OpenStack network.
After terraform apply, OpenStack will allocate an available public IP from the specified pool.
Associating a Floating IP with a virtual machine
Creating a Floating IP is not enough — it must be associated with a port or a VM instance.
One option is to use a separate association resource:
resource "openstack_compute_floatingip_associate_v2" "vm" {
floating_ip = openstack_networking_floatingip_v2.vm.address
instance_id = openstack_compute_instance_v2.vm.id
}
Here, Terraform uses openstack_networking_floatingip_v2.vm.address from the created Floating IP and associates it with openstack_compute_instance_v2.vm.id
This introduces another dependency:

In practice, the exact association method may depend on the OpenStack Provider version and the cloud network design. In some configurations, it is more convenient to work through the VM port and the openstack_networking_floatingip_associate_v2 resource.
The principle remains the same: the public IP must be associated with the network interface of the created virtual machine.
Displaying the public IP via an output
To avoid looking up the address manually in the OpenStack dashboard, add an output.
Open: nano outputs.tf
Add:
output "floating_ip" {
description = "Public Floating IP of the Terraform VM"
value = openstack_networking_floatingip_v2.vm.address
}
You can also output the VM’s private address:
output "private_ip" {
description = "Private IP of the Terraform VM"
value = openstack_compute_instance_v2.vm.access_ip_v4
}
After a successful terraform apply, Terraform automatically displays the outputs at the end of the run.
You can retrieve them separately later: terraform output
Or retrieve only the Floating IP: terraform output floating_ip
Outputs are useful for addresses, IDs, and other values you will need after the infrastructure has been created.
Reviewing a Terraform plan

Before creating resources, Terraform lets you validate the configuration and preview the changes that will be made. This step is especially important for cloud infrastructure, because an error in the code can not only cause a command to fail, but also lead to the creation or deletion of billable resources.
Formatting and Checking the Configuration
First, format the .tf files using the standard Terraform style: terraform fmt
This command automatically fixes indentation and HCL formatting.
To check which files were changed, run: terraform fmt -check
If the command completes with no output and no errors, the formatting is correct.
Before proceeding, it is also useful to review the file list: ls -la
The directory should contain at least the following files:
providers.tf
network.tf
security.tf
instance.tf
outputs.tf
variables.tf
Let’s move on.
Running terraform validate
Now let’s check the syntax and internal consistency of the configuration: terraform validate
If the configuration is correct, Terraform will return: Success! The configuration is valid.
validate checks the HCL structure and the validity of the provider arguments, but it does not guarantee that the specified image, flavor, external network, or keypair actually exists in the specific OpenStack project.
These errors may appear later during plan or apply.
Reviewing changes with terraform plan
Generate a plan: terraform plan
Terraform will:
- Load the current state;
- Call the OpenStack API;
- Compare the actual resources with the configuration;
- Build a list of the planned changes.
Since the infrastructure does not exist yet, most resources will be marked as: + create
At the end, a summary similar to the following will appear: Plan: 8 to add, 0 to change, 0 to destroy.
The number of resources depends on how many individual Security Group rules and association resources are defined in the configuration.
For a more controlled apply, you can save the plan to a file: terraform plan -out=tfplan
After that, apply this exact reviewed plan with the command: terraform apply tfplan
This way, Terraform will not build a different plan between review and apply.
What resources Terraform is going to create
The terraform plan output should include the main resource types:
openstack_networking_network_v2.private
openstack_networking_subnet_v2.private
openstack_networking_secgroup_v2.vm
openstack_networking_secgroup_rule_v2.ssh
openstack_compute_instance_v2.vm
openstack_networking_floatingip_v2.vm
If HTTP, HTTPS, and ICMP rules have been added, they will also be shown as separate objects.
For each resource, Terraform shows the planned parameters. For example, for the network: + name = “terraform-network”
for the subnet: + cidr = “192.168.50.0/24”
for the VM: + name = “terraform-vm”
and the Floating IP address is usually still unknown at the plan stage and is displayed as a value that will be determined after apply.
Before proceeding, carefully check:
- Image name;
- Flavor;
- Keypair;
- Network CIDR;
- Security group rules;
- External network;
- Number of resources to be created.
If the plan contains an unexpected destroy operation or mass resource recreation, do not run apply until the cause is understood.
Creating infrastructure with terraform apply
After reviewing the plan, you can proceed to create the resources.
Terraform will call the OpenStack API and perform operations in an order determined by the dependencies between objects.
Running terraform apply
If a plan was previously saved: terraform plan -out=tfplan
apply that exact plan: terraform apply tfplan
If no separate plan file was created, you can run: terraform apply
In this case, Terraform will create the plan again and show it before confirmation.
For infrastructure changes, it is preferable to first run terraform plan separately, review the output, and only then run apply.
Confirming changes
When running the standard command: terraform apply
Terraform will ask for confirmation: Do you want to perform these actions?
To continue, enter: yes
Resource creation will then begin.
The output will show the following lines in sequence:
Creating…
Still creating…
Creation complete
Terraform can create independent objects in parallel. For example, a Security Group and a private network do not necessarily need to wait for each other.
Resources with explicit dependencies, however, will be created in the correct order.
Verifying Created Resources
After successful execution, Terraform will report the final status: Apply complete!
For example: Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
You can now verify the infrastructure with Terraform: terraform state list
The created resources should appear in the list.
Additionally, if the OpenStack CLI is installed, you can verify them directly through the API:
openstack network list
openstack server list
openstack floating ip list
This lets you verify that the Terraform state matches the actual OpenStack objects.
Retrieving output values after apply completes
At the end of terraform apply, Terraform automatically displays the values from outputs.tf.
For example:
Outputs: floating_ip = "203.0.113.50"
In a real project, the address will be assigned automatically by OpenStack.
You can retrieve the output values again with the following command: terraform output
Expected result: floating_ip = "203.0.113.50"
If you need to retrieve the value without additional formatting: terraform output -raw floating_ip
This is useful for passing the result to shell scripts: ssh ubuntu@$(terraform output -raw floating_ip)
Thus, after terraform apply, Terraform not only creates all of the described infrastructure but also immediately provides the key values required for connecting to and verifying the VM.
Validating the Created Infrastructure
After running terraform apply, it is important to check not only Terraform’s final message, but also the actual state of the resources in OpenStack. This confirms that the network has been created, the virtual machine has the required interfaces, the Floating IP is associated correctly, and the Security Group is actually applied.
Verifying Resources Through OpenStack
If the OpenStack CLI is installed and the environment variables have already been loaded, you can retrieve the list of created resources directly through the API.
Check the networks: openstack network list
The list should include: terraform-network
Check the subnets: openstack subnet list
Expected: terraform-subnet
Check the virtual machines: openstack server list
The list should show: terraform-vm
Check the Floating IP: openstack floating ip list
And the Security Group: openstack security group list
If the resources are present and their parameters match the configuration, Terraform has successfully created the infrastructure through the OpenStack API.
Connecting to a virtual machine via a Floating IP
Retrieve the public address from the output: terraform output -raw floating_ip
You can then connect over SSH: ssh -i ~/.ssh/terraform-key ubuntu@$(terraform output -raw floating_ip)
If the image uses a different user, for example:
debian
centos
rocky
replace the username in the command.
If the connection is successful, you will get a shell on the created virtual machine.
For an additional check, you can run the following inside the VM:
hostname
ip addr
ip route
This lets you verify that the machine has actually received an address from the private network and has the correct route.
Checking the Network and Security Group
To verify the network configuration, first check the VM addresses: openstack server show terraform-vm
The server details should show the private IP address and, if the association was completed correctly, the Floating IP address.
You can also inspect the port: openstack port list –server terraform-vm
And the Security Group rules: openstack security group rule list terraform-vm-sg
The list should include the rules created by Terraform, for example:
TCP 22
TCP 80
TCP 443
ICMP
In practice, it is useful to verify separately that only the required ports are allowed.
If SSH works and no other administrative services are exposed, the Security Group is configured according to the expected design.
How Terraform Stores Infrastructure State
After creating resources, Terraform must remember which real OpenStack objects correspond to each resource block in the configuration.
Terraform uses state for this.
What terraform.tfstate contains
By default, the local state is stored in terraform.tfstate
This is a structured file in which Terraform stores information about the resources it manages.
The state can contain:
- Network ID;
- Subnet ID;
- Virtual machine ID;
- Security Group ID;
- Floating IP address;
- Resource attributes;
- Dependencies between objects;
- Outputs;
- Provider metadata.
For example, Terraform maps the logical resource openstack_compute_instance_v2.vm
to a specific virtual machine ID in OpenStack.
This is why the next time terraform plan runs, Terraform knows that the VM already exists and should not be created again.
Why the state file should not be edited manually
terraform.tfstate is not a regular configuration file.
Although it is technically JSON, you should not modify its contents manually. An error in an ID or in the structure can prevent Terraform from correctly mapping the configuration to the real infrastructure.
The consequences can vary:
- Terraform may decide to create a duplicate resource;
- An existing object may no longer be tracked;
- The plan may show an unexpected replacement;
- A destroy operation may not affect the intended resource;
- The project state may become inconsistent.
There are dedicated commands for controlled changes to state:
terraform state list
terraform state show RESOURCE
terraform state mv
terraform state rm
These commands should also be used with care, but they modify the state structure correctly.
Why a state file may contain sensitive data
The state file contains the actual values of resource attributes, not just object names.
As a result, it may include:
- IP addresses;
- Internal identifiers;
- Metadata;
- Configuration values;
- Outputs;
- Resource parameters;
- Certain values passed through provider or resource arguments.
Even if a value in Terraform is marked as sensitive = true,
this primarily hides it from standard CLI output. The value itself may still be present in the state when required.
Therefore, the state file should be treated as potentially sensitive.
Where to store state when working in a team
For a single-person test project, a local terraform.tfstate file is acceptable.
However, in a team environment, storing state locally quickly creates problems:
- Different team members end up with different versions of the state;
- Two people can run apply at the same time;
- The state can easily be lost;
- Centralized access control is difficult to implement.
For this reason, production infrastructure typically uses a remote backend.
It allows the state to be stored centrally and, depending on the backend, provides additional mechanisms such as state locking.
The specific backend depends on the team’s infrastructure. It may be a compatible object store, a dedicated Terraform backend, or another supported service.
The key requirement is that the state must be stored centrally, with access control and backups in place.
Why terraform.tfstate must not be published to Git
State is not part of the project’s source code.
It must not be stored in a public Git repository because:
- It may contain sensitive values;
- State changes regularly;
- Git does not solve the problem of concurrent access;
- Old sensitive information remains in the commit history even after it has been removed from the current version.
For this reason, we added the following to .gitignore in advance:
*.tfstate
*.tfstate.*
By contrast, the .terraform.lock.hcl file should usually be stored in the repository because it pins the selected provider versions.
Let’s check which resources Terraform is currently managing: terraform state list
Example:
openstack_compute_floatingip_associate_v2.vm
openstack_compute_instance_v2.vm
openstack_networking_floatingip_v2.vm
openstack_networking_network_v2.private
openstack_networking_secgroup_rule_v2.http
openstack_networking_secgroup_rule_v2.https
openstack_networking_secgroup_rule_v2.icmp
openstack_networking_secgroup_rule_v2.ssh
openstack_networking_secgroup_v2.vm
openstack_networking_subnet_v2.private
This list is especially useful before modifying or deleting infrastructure.
Changing Existing Infrastructure

Terraform is not intended solely for the initial creation of resources. The same configuration is also used for subsequent changes.
Simply update the desired state in the .tf file and run terraform plan again.
How Terraform Determines Changes
Assume that the security group initially allows SSH, ICMP, and HTTPS.
Later, HTTP needs to be added.
A new resource appears in security.tf:
resource "openstack_networking_secgroup_rule_v2" "http" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 80
port_range_max = 80
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.vm.id
}
Terraform compares:
- The configuration;
- The state;
- The actual state in OpenStack.
It then determines that the existing objects are preserved and that only one new rule needs to be created.
Rerunning terraform plan
After changing the configuration, run the following again:
terraform fmt
terraform validate
Then: terraform plan
If the change only affects a new HTTP rule, the result may look like this: Plan: 1 to add, 0 to change, 0 to destroy.
This is one of Terraform’s most useful features: before applying changes, you can see whether they will affect resources that are already running.
If, instead of the expected change, you see: -/+ destroy and then create replacement
you need to carefully check the reason. Some resource attributes cannot be changed without recreating the resource.
Applying Changes Without Recreating the Entire Infrastructure
If the plan matches your expectations, apply the changes: terraform apply
Terraform will modify only the objects whose state differs from the configuration.
For example, adding a single Security Group rule does not require recreating:
Network
Subnet
VM
Floating IP
They will remain unchanged.
After the operation completes, you can check terraform state list and terraform plan again.
If the infrastructure fully matches the configuration, Terraform will report:
No changes. Your infrastructure matches the configuration.
This is how Terraform lets you use the same .tf files to create cloud infrastructure and then make subsequent managed changes to it, without recreating all resources for every edit.
Deleting Infrastructure Using Terraform
Terraform manages not only the creation and modification of resources, but also their deletion. The terraform destroy command is used for this purpose.
This command is especially risky in production infrastructure because Terraform deletes real cloud resources that it manages. Therefore, you must carefully review the plan before confirming.
Reviewing resources before deletion
First, check which objects are in the state: terraform state list
For example:
openstack_compute_floatingip_associate_v2.vm
openstack_compute_instance_v2.vm
openstack_networking_floatingip_v2.vm
openstack_networking_network_v2.private
openstack_networking_secgroup_rule_v2.http
openstack_networking_secgroup_rule_v2.https
openstack_networking_secgroup_rule_v2.icmp
openstack_networking_secgroup_rule_v2.ssh
openstack_networking_secgroup_v2.vm
openstack_networking_subnet_v2.private
This lets you understand in advance which resources Terraform considers part of the project.
To preview what will be deleted, run: terraform plan -destroy
Terraform will generate a plan in which the resources are marked for deletion: – destroy
At the end, you will see a summary similar to this: Plan: 0 to add, 0 to change, 10 to destroy.
The number depends on the project’s actual configuration.
Running terraform destroy
To destroy the managed infrastructure, use: terraform destroy
Terraform will display the plan again and ask for confirmation: Do you really want to destroy all resources?
To continue, you must explicitly enter: yes
The deletion process will then begin.
The console will display messages such as:
Destroying…
Still destroying…
Destruction complete
Terraform takes dependencies between objects into account and attempts to delete resources in the appropriate order.
For example, the Floating IP association must be removed before the VM itself is deleted, and a network cannot be deleted while related resources are still attached to it.
Resources that will be deleted
terraform destroy affects all resources in the current configuration that Terraform manages in its state.
In our example, these are:
- Virtual machine;
- Floating IP and its association;
- Security Group;
- Security Group rules;
- Private network;
- Subnet.
If additional disks, ports, routers, or other OpenStack objects are added to the project later, they will also be included in the destroy operation if they are managed by this Terraform state.
Before confirming, it is especially important to check the summary line: 0 to add, 0 to change, N to destroy
If the number of objects to be deleted is unexpectedly high, it is better to cancel the operation and first review the configuration and state.
Consequences of Deleting a Virtual Machine, Network, and Floating IP
Deleting infrastructure has real consequences.
When a VM is destroyed, the compute instance itself is removed. Data stored only on its local ephemeral disk may be lost after deletion.
If a separate persistent volume is also defined in Terraform and included in the destroy operation, it may also be deleted depending on the resource configuration.
When a Floating IP is deleted, the address is released and returned to the provider’s pool. After the infrastructure is recreated, OpenStack may assign a different public IP.
Deleting the private network and subnet disrupts the project’s network configuration. If resources that Terraform does not track are manually attached to these objects, deletion may fail because of existing dependencies.
Therefore, terraform destroy should not be treated as routine cleanup of a local project. The command modifies real cloud infrastructure.
What happens to the state after destroy
After a successful destroy operation, Terraform updates the state and removes records of the destroyed objects from it.
If all project resources have been deleted, the terraform state list command should not return any managed objects.
At the end of the run, the following message will appear: Destroy complete! Resources: 10 destroyed.
The terraform.tfstate file itself may remain in the working directory. It will simply reflect the new state, in which the previously created resources no longer exist.
Do not delete the state manually before running terraform destroy. If Terraform loses its connection to the resources before they are deleted, the cloud objects may continue to exist but stop being managed by the current project.
Working Securely with Terraform and OpenStack

Terraform can create and delete real resources, so project security includes not only protecting the OpenStack account, but also properly managing configuration, state, and change plans.
Storing credentials outside .tf files
Do not put the OpenStack password directly in the provider block:
provider "openstack" {
user_name = "admin"
password = "secret-password"
}
A file like this can easily be committed to Git, added to an archive, or captured by a backup system by accident.
Instead, it is better to use:
- OS_* environment variables;
- OpenRC;
- Application credentials;
- A protected clouds.yaml;
- A secret manager in larger infrastructure.
As a result, the Terraform code remains suitable for publication and team collaboration without hardcoded credentials.
When using application credentials, you should also create separate credentials with only the minimum required permissions instead of using the primary administrator account.
Using .gitignore for state files and local variables
A minimal .gitignore file for a Terraform project might look like this:
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
crash.log
If needed, local override files and other temporary data are also added to it.
For example:
override.tf
override.tf.json
*_override.tf
*_override.tf.json
.terraform.lock.hcl is usually kept in Git.
This keeps the configuration and pinned provider versions in the repository, but not local state or secret values.
Protecting the state file and backups
Because the state file can contain sensitive data, access to it must be restricted.
For a local project, you can check the permissions with: ls -l terraform.tfstate
If necessary, restrict them with: chmod 600 terraform.tfstate
If the state file is stored remotely, you should use:
- A private backend;
- Access control;
- Encryption;
- Versioning or backups;
- State locking, if the backend supports it.
State backups are also considered sensitive. You must not protect the main file while leaving copies such as terraform.tfstate.backup in a publicly accessible directory.
Checking the plan before apply and destroy
The key rule when working with Terraform is not to apply infrastructure changes blindly.
Before creating or modifying resources: terraform plan
Before deleting resources: terraform plan -destroy
Check the following:
- Which resources will be added;
- Which resources will be changed;
- Which resources will be destroyed;
- Whether any replacement is planned;
- Whether the expected parameters are changing;
- Whether any unexpected destroy action appears.
Pay particular attention to the notation: -/+
It means that the existing object will be destroyed and recreated.
For a virtual machine, disk, or public IP address, this can lead to downtime or data loss.
Maintaining a Terraform Project
After the initial deployment, the Terraform project continues to be used to validate, update, and modify the infrastructure.
Viewing the Current State with terraform state
You can get a list of managed resources with: terraform state list
To view details for a specific resource: terraform state show openstack_compute_instance_v2.vm
The command displays the VM parameters known to Terraform.
The following command is also useful: terraform show
It outputs the current project state in a more comprehensive form.
However, state commands are intended primarily for diagnostics. Day-to-day infrastructure changes are best made through the .tf configuration and terraform apply, rather than by manipulating the state directly.
Updating a provider
Provider versions are locked in .terraform.lock.hcl
You can check the providers in use with: terraform providers
If you need to update them within the allowed constraints, run: terraform init -upgrade
After the update, run terraform validate and terraform plan again.
Before applying the changes, make sure the new provider version does not propose unexpectedly recreating any resources.
In production infrastructure, it is best to test provider updates on a test project first.
What to do when the Terraform state differs from the actual infrastructure
Drift can occur if a resource is modified or deleted manually through the OpenStack Dashboard, bypassing Terraform.
For example, if a VM is deleted manually but its entry remains in the state, the next terraform plan will detect the difference.
Depending on the situation, Terraform may propose recreating the deleted object or making another change to bring the infrastructure back to the declared configuration.
For this reason, it is best to avoid making manual changes to resources managed by Terraform.
If an existing OpenStack object needs to be brought under Terraform management, use import, for example: terraform import RESOURCE_ADDRESS OPENSTACK_ID
After the import, the .tf configuration must match the resource’s actual parameters.
If the object should no longer be tracked but should remain in OpenStack, use: terraform state rm RESOURCE_ADDRESS
Perform this operation only after checking the impact: Terraform will forget the resource, but it will not delete it from the cloud.
What to Do If terraform apply Fails
A terraform apply error does not necessarily mean that nothing was created.
Terraform performs operations incrementally, so some resources may have been created before the error occurred.
First, review the Terraform message and identify the problematic resource.
Then check the state: terraform state list
And run again: terraform plan
Terraform will compare the current configuration with what has already been created and show the remaining actions.
If the error is related, for example, to an incorrect flavor name or external network name, fix the configuration and run again: terraform apply
Terraform should not recreate resources that have already been created successfully and correctly recorded in state.
For authentication issues, check that the variables are present: env | grep ‘^OS_’ | sed ‘s/=.*$/=<set>/’
If the error relates to a specific OpenStack resource, it is also useful to check its status through the OpenStack CLI or the provider’s dashboard.
The main thing is not to delete terraform.tfstate in an attempt to “start over.” This can only make the situation worse and leave already created cloud objects unlinked from Terraform.
Conclusion

Terraform lets you describe OpenStack infrastructure as code and manage its entire lifecycle through a single project. Instead of creating resources manually in the dashboard, the configuration defines the desired state, and the OpenStack Provider performs the required operations through the API. Terraform itself uses state to map objects from .tf files to actual cloud resources.
In this guide, we defined a private network and subnet, a Security Group, a virtual machine, and a Floating IP, then reviewed the sequence of terraform init, plan, apply, output, and destroy. This approach lets you review the planned changes in advance and apply them in a controlled manner, and, when necessary, completely remove the infrastructure created by Terraform.
Special attention should be paid to terraform.tfstate. This file is required for Terraform to manage resources and may contain sensitive values, so it must not be published to Git without careful consideration. For team-based work, it is better to use a secure remote backend with access controls and, if the backend supports it, state locking.
Finally, terraform destroy should be treated as a full-fledged infrastructure operation, not as local project cleanup. The command deletes real resources managed by the current state. Before confirming deletion, you must review the destroy plan and consider the potential loss of VMs, addresses, and data that has not been saved separately.
FAQ
Why does Terraform need a state file?
Terraform uses state to map resources in the configuration to real infrastructure objects. It stores the information needed to determine future changes. Without an accurate state file, Terraform can lose track of resources it created earlier.
Can I delete terraform.tfstate and simply run terraform apply again?
You should not do that. If the actual OpenStack resources still exist but the state has been lost, Terraform will no longer know exactly what it created. As a result, a new plan may propose creating additional objects instead of managing the existing ones.
If the state is corrupted or the infrastructure has diverged from it, you should first assess the current situation and, if necessary, use terraform import or the terraform state commands.
Why shouldn’t terraform.tfstate be stored in a public Git repository?
Local Terraform state is stored in plaintext and may contain secrets or other sensitive values. Even values marked as sensitive may be present in the state file, although Terraform hides them in standard CLI output. For this reason, state files must be excluded from Git and protected as sensitive files.
Can .terraform.lock.hcl be stored in Git?
Yes. Unlike state, the dependency lock file is intended to pin the selected provider versions and is typically stored alongside the configuration. This helps different environments use the same dependencies.
Why run terraform plan before terraform apply?
terraform plan shows the actions Terraform intends to take to bring the infrastructure into line with the declared configuration. This lets you identify any unexpected creation, modification, recreation, or deletion of resources before the cloud environment is actually changed. terraform apply then performs the operations proposed by the plan.
How does terraform plan -destroy differ from terraform destroy?
terraform plan -destroy only shows the proposed destroy plan and does not modify the infrastructure. terraform destroy deletes the managed resources. Therefore, before destroying infrastructure, it is useful to review a separate destroy plan first.
Will terraform destroy delete all resources in an OpenStack project?
No. The command uses the current Terraform configuration and state. It deletes the resources managed by that Terraform project, not arbitrary objects across the entire OpenStack project.
What happens to a Floating IP after running terraform destroy?
If the Floating IP was created as a Terraform resource and is included in the current state, Terraform will request that it be deleted along with the rest of the infrastructure. After the address is released, it may return to the OpenStack pool, so the same IP may not necessarily be assigned the next time the infrastructure is created.
Can OpenStack credentials be passed via environment variables?
Yes. The OpenStack Provider supports standard environment variables, including project, user, and application credentials parameters. This lets you avoid placing passwords and secrets directly in .tf files.
Where should state be stored when working as a team?
For a shared Terraform project, a remote backend with access control is preferred. Backends are responsible for storing state and may provide a state locking mechanism. If locking is supported, Terraform uses it to prevent multiple operations from writing to the state at the same time.
Can manual management and IaC be combined?
If the infrastructure is managed with Terraform, manual changes made through the web interface or the OpenStack API will cause the recorded state to diverge from the actual infrastructure. This, in turn, can lead to errors or the removal of manual changes, so manual changes are not recommended. However, if Terraform is used to manage virtual machines but not networks and routing, manual changes to networks will not affect the state and are acceptable.
What should you do if terraform apply fails halfway through?
Do not delete the state and start the project over. Some resources may already have been created successfully and recorded in the state. First, review the error message, run terraform state list and a new terraform plan, fix the problematic configuration, and then run terraform apply again.
Terraform applies the actions from the plan, so after a partially completed operation, the next plan helps identify which changes are still required.
