# Terraform and OpenTofu on OpenStack: Getting Started with the OpenStack Provider

> Deploy your first resources on OpenStack with Terraform or OpenTofu: clouds.yaml authentication, a network and router, an instance and a floating IP.

- Source: https://www.itwonderlab.com/terraform-openstack-getting-started/
- Published: 2026-08-10
- Updated: 2026-08-10
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## OpenStack with Terraform

[OpenStack](https://www.itwonderlab.com/openstack/) is open source software to build clouds, not a single cloud. Companies run it in their own data centers (private clouds), and several providers offer public clouds based on it. Every OpenStack cloud has its own endpoint, its own image and flavor names and its own external network, so the same Terraform code needs small changes from one cloud to another. The tutorial shows which values to look up.

Terraform and OpenTofu use the `openstack` [provider](https://www.itwonderlab.com/terraform-provider/), maintained by the community under the `terraform-provider-openstack` organization. It covers the compute (Nova), networking (Neutron), image (Glance), block storage (Cinder) and identity (Keystone) services. The workflow is the same as for [AWS](https://www.itwonderlab.com/aws-terraform-tutorial-terraform-basics/), and the networking is explicit like in a data center: a network, a subnet, a router and a floating IP.

### Authentication with clouds.yaml

Ask your cloud administrator or download from the dashboard (the Horizon web interface) one of these two files:

- **`clouds.yaml`**, the format of the OpenStack SDK, which keeps several clouds in one file.
- **An `openrc` file**, a shell script that sets `OS_*` environment variables.

With `clouds.yaml`, saved in `~/.config/openstack/clouds.yaml`, choose the cloud by name. An entry looks like this (the values come from your cloud):

```yaml title="clouds.yaml"
clouds:
  mycloud:
    auth:
      auth_url: https://keystone.example.com:5000/v3
      application_credential_id: "<id>"
      application_credential_secret: "<secret>"
    region_name: RegionOne
    auth_type: v3applicationcredential
```

Use an **application credential** (Identity menu in the dashboard) when your cloud supports it: it can be limited and revoked without changing your password. Then select the cloud with an environment variable:

```shell
$ export OS_CLOUD=mycloud
```

With an `openrc` file run `source openrc.sh` instead, and the provider reads `OS_AUTH_URL`, `OS_USERNAME`, `OS_REGION_NAME` and the other variables.

### Provider

```hcl title="providers.tf"
terraform {
  required_version = ">= 1.6"

  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 3.4"
    }
  }
}

provider "openstack" {
  # the cloud is selected with OS_CLOUD (clouds.yaml) or OS_* variables (openrc)
}

variable "external_network_name" {
  type        = string
  description = "Name of the external (public) network of your cloud, see: openstack network list --external"
}

variable "image_name" {
  type        = string
  description = "Image name, see: openstack image list"
}

variable "flavor_name" {
  type        = string
  description = "Flavor name, see: openstack flavor list"
}

variable "floating_ip_pool" {
  type        = string
  description = "Floating IP pool, usually the name of the external network"
}

variable "admin_cidr" {
  type        = string
  description = "Address range that can connect with SSH, for example your public IP as x.x.x.x/32"
}

variable "ssh_public_key_path" {
  type    = string
  default = "~/.ssh/id_ed25519.pub"
}
```

The names change in every cloud. The OpenStack command line client shows them: `openstack network list --external`, `openstack image list` and `openstack flavor list`.

### Look up existing resources

```hcl title="data.tf"
data "openstack_networking_network_v2" "external" {
  name     = var.external_network_name
  external = true
}

data "openstack_images_image_v2" "os" {
  name        = var.image_name
  most_recent = true
}

data "openstack_compute_flavor_v2" "small" {
  name = var.flavor_name
}
```

### Network, subnet and router

A tenant network is private. To reach the internet, connect it to a router that has a gateway on the external network:

```hcl title="network.tf"
resource "openstack_networking_network_v2" "main" {
  name           = "ditwl-demo"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "main" {
  name            = "ditwl-demo"
  network_id      = openstack_networking_network_v2.main.id
  cidr            = "10.0.1.0/24"
  ip_version      = 4
  dns_nameservers = ["1.1.1.1", "9.9.9.9"]
}

resource "openstack_networking_router_v2" "main" {
  name                = "ditwl-demo"
  admin_state_up      = true
  external_network_id = data.openstack_networking_network_v2.external.id
}

resource "openstack_networking_router_interface_v2" "main" {
  router_id = openstack_networking_router_v2.main.id
  subnet_id = openstack_networking_subnet_v2.main.id
}
```

### Security group

```hcl title="security.tf"
resource "openstack_networking_secgroup_v2" "web" {
  name        = "ditwl-demo-web"
  description = "SSH from the admin range"
}

resource "openstack_networking_secgroup_rule_v2" "ssh" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = var.admin_cidr
  security_group_id = openstack_networking_secgroup_v2.web.id
}
```

A new Neutron security group normally starts with rules that allow outgoing traffic, so only the ingress rule is added here. Check the rules of your cloud with `openstack security group rule list`.

### Key pair, port and instance

The instance uses a **port** that is created first, so that the security group and the floating IP are attached to the port:

```hcl title="compute.tf"
resource "openstack_compute_keypair_v2" "main" {
  name       = "ditwl-demo"
  public_key = file(pathexpand(var.ssh_public_key_path))
}

resource "openstack_networking_port_v2" "web" {
  name               = "ditwl-demo-web"
  network_id         = openstack_networking_network_v2.main.id
  admin_state_up     = true
  security_group_ids = [openstack_networking_secgroup_v2.web.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.main.id
  }
}

resource "openstack_compute_instance_v2" "web" {
  name      = "ditwl-demo-web"
  image_id  = data.openstack_images_image_v2.os.id
  flavor_id = data.openstack_compute_flavor_v2.small.id
  key_pair  = openstack_compute_keypair_v2.main.name

  network {
    port = openstack_networking_port_v2.web.id
  }
}
```

### Floating IP

A floating IP is a public address that the cloud maps to the private address of the port:

```hcl title="floating-ip.tf"
resource "openstack_networking_floatingip_v2" "web" {
  pool = var.floating_ip_pool
}

resource "openstack_networking_floatingip_associate_v2" "web" {
  floating_ip = openstack_networking_floatingip_v2.web.address
  port_id     = openstack_networking_port_v2.web.id
}

output "web_public_ip" {
  value = openstack_networking_floatingip_v2.web.address
}
```

The router must be connected to the subnet before the floating IP can be associated. The provider normally works out this order, and if you see an error about the port not being reachable from the external network, add `depends_on = [openstack_networking_router_interface_v2.main]` to the association.

### Run it

Put the values of your cloud in a `terraform.tfvars` file. The ones below are examples:

```hcl title="terraform.tfvars"
external_network_name = "public"
floating_ip_pool      = "public"
image_name            = "Ubuntu 24.04"
flavor_name           = "m1.small"
admin_cidr            = "203.0.113.25/32"
```

```shell
$ tofu init
$ tofu plan
$ tofu apply
$ tofu destroy
```

Use `terraform` instead of `tofu` if you prefer HashiCorp Terraform. Connect with `ssh <user>@<floating ip>`, where the user depends on the image (`ubuntu` for the Ubuntu cloud images).

### Next steps

- Initialize the instance with [cloud-init](https://www.itwonderlab.com/cloud-init/) using the `user_data` argument.
- Use a [backend](https://www.itwonderlab.com/terraform-backend/) for the state, for example an S3-compatible bucket if your cloud offers Swift or Ceph object storage with an S3 API.
- Read [project structure](https://www.itwonderlab.com/terraform-project-structure/) and [best practices](https://www.itwonderlab.com/terraform-best-practices/).
