Skip to content

Complete Setup with Terraform & Ansible

This guide provides two complete, copy-paste-ready infrastructure-as-code examples that deploy a VM with network, security group, data volume, and floating IP — one using Terraform, the other using Ansible. Both create the same architecture; pick whichever tool fits your workflow. The Ansible playbook additionally configures the VM (formats the data volume, installs Nginx).

Architecture Overview

%%{init: {'themeVariables': {'textColor': '#333333', 'edgeLabelBackground': '#ffffff', 'clusterBkg': 'transparent', 'clusterBorder': '#999999'}}}%%
flowchart TD
    Internet([Internet]) <-->|FIP| FIP["Floating IP"]
    FIP <-->|NAT| Router

    subgraph Provider ["Provider Network"]
        ExtNet["External Network (public)"]
    end

    subgraph Project ["Project"]
        Router["Router"]
        Router <-->|Gateway| ExtNet

        subgraph Net ["Private Network"]
            Subnet["Subnet 192.168.100.0/24"]
            Router <-->|Interface| Subnet

            subgraph SG ["Security Group"]
                VM["VM Instance"]
            end
        end

        Vol["Data Volume (100 GB)"]
        Vol <-.->|Attached| VM
    end

    classDef vm fill:#d4edda,stroke:#28a745,color:#1b5e20,stroke-width:2px;
    classDef router fill:#fff3cd,stroke:#ffc107,color:#6d4c00,stroke-width:2px;
    classDef ext fill:#f8d7da,stroke:#dc3545,color:#8b0000,stroke-width:2px;
    classDef vol fill:#e3f2fd,stroke:#1976d2,color:#0d47a1,stroke-width:2px;
    classDef net fill:#eeeeee,stroke:#757575,stroke-dasharray: 5 5;
    class VM vm;
    class Router router;
    class ExtNet ext;
    class Vol vol;
    class Subnet net;
%%{init: {'themeVariables': {'textColor': '#e0e0e0', 'edgeLabelBackground': '#2a2a2a', 'clusterBkg': 'transparent', 'clusterBorder': '#666666'}}}%%
flowchart TD
    Internet([Internet]) <-->|FIP| FIP["Floating IP"]
    FIP <-->|NAT| Router

    subgraph Provider ["Provider Network"]
        ExtNet["External Network (public)"]
    end

    subgraph Project ["Project"]
        Router["Router"]
        Router <-->|Gateway| ExtNet

        subgraph Net ["Private Network"]
            Subnet["Subnet 192.168.100.0/24"]
            Router <-->|Interface| Subnet

            subgraph SG ["Security Group"]
                VM["VM Instance"]
            end
        end

        Vol["Data Volume (100 GB)"]
        Vol <-.->|Attached| VM
    end

    classDef vm fill:#1b3a28,stroke:#66bb6a,color:#c8e6c9,stroke-width:2px;
    classDef router fill:#4d3a00,stroke:#ffb300,color:#ffe082,stroke-width:2px;
    classDef ext fill:#4a1a1a,stroke:#ef5350,color:#ffcdd2,stroke-width:2px;
    classDef vol fill:#0d2944,stroke:#42a5f5,color:#bbdefb,stroke-width:2px;
    classDef net fill:#2a2a2a,stroke:#9e9e9e,stroke-dasharray: 5 5;
    class VM vm;
    class Router router;
    class ExtNet ext;
    class Vol vol;
    class Subnet net;

Prerequisites

Project Structure

openstack-demo/
├── main.tf          # Terraform: all infrastructure + VM
├── playbook.yml     # Ansible: all infrastructure + VM + configuration (standalone)
└── clouds.yaml      # application credentials (not in version control)

Standalone approaches

main.tf and playbook.yml are independent — you can use either one without the other. They deploy the same resources.

Terraform — main.tf

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

provider "openstack" {
  cloud = "openstack"
}

# ── Network ──────────────────────────────────────────────

resource "openstack_networking_network_v2" "private" {
  name           = "demo-network"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "subnet" {
  name        = "demo-subnet"
  network_id  = openstack_networking_network_v2.private.id
  cidr        = "192.168.100.0/24"
  ip_version  = 4
  dns_nameservers = ["8.8.8.8", "8.8.4.4"]
}

resource "openstack_networking_router_v2" "router" {
  name                = "demo-router"
  external_network_id = data.openstack_networking_network_v2.public.id
}

resource "openstack_networking_router_interface_v2" "router_iface" {
  router_id = openstack_networking_router_v2.router.id
  subnet_id = openstack_networking_subnet_v2.subnet.id
}

# ── Security Group ───────────────────────────────────────

resource "openstack_compute_secgroup_v2" "secgroup" {
  name        = "demo-secgroup"
  description = "Allow SSH and ICMP"

  rule {
    from_port   = 22
    to_port     = 22
    ip_protocol = "tcp"
    cidr        = "0.0.0.0/0"
  }

  rule {
    from_port   = -1
    to_port     = -1
    ip_protocol = "icmp"
    cidr        = "0.0.0.0/0"
  }
}

# ── Data Sources ─────────────────────────────────────────

data "openstack_networking_network_v2" "public" {
  name = "public"
}

data "openstack_images_image_v2" "ubuntu" {
  name        = "Ubuntu 24.04"
  most_recent = true
}

# ── Data Volume ──────────────────────────────────────────

resource "openstack_blockstorage_volume_v3" "data" {
  name              = "demo-data-vol"
  size              = 100
  volume_type       = "v-ssd-std"
  availability_zone = "ch-zh1-az2"
}

# ── Instance ─────────────────────────────────────────────

resource "openstack_compute_instance_v2" "vm" {
  name              = "demo-vm"
  image_id          = data.openstack_images_image_v2.ubuntu.id
  flavor_name       = "g1.2c4m"
  key_pair          = "my-key"
  security_groups   = [openstack_compute_secgroup_v2.secgroup.name]
  availability_zone = "ch-zh1-az2"

  network {
    uuid = openstack_networking_network_v2.private.id
  }
}

resource "openstack_compute_volume_attach_v2" "attach" {
  instance_id = openstack_compute_instance_v2.vm.id
  volume_id   = openstack_blockstorage_volume_v3.data.id
}

# ── Floating IP ──────────────────────────────────────────

resource "openstack_networking_floatingip_v2" "fip" {
  pool = data.openstack_networking_network_v2.public.name
}

resource "openstack_compute_floatingip_associate_v2" "fip_assoc" {
  floating_ip = openstack_networking_floatingip_v2.fip.address
  instance_id = openstack_compute_instance_v2.vm.id
}

# ── Outputs ──────────────────────────────────────────────

output "floating_ip" {
  value = openstack_networking_floatingip_v2.fip.address
}

output "vm_name" {
  value = openstack_compute_instance_v2.vm.name
}

output "volume_id" {
  value = openstack_blockstorage_volume_v3.data.id
}

Deploy

cd openstack-demo
terraform init
terraform plan
terraform apply

Terraform outputs the floating IP after a successful apply:

floating_ip = "185.123.45.67"
vm_name     = "demo-vm"
volume_id   = "71bd47d8-b51e-4f12-9c51-0445fc36c9e4"

Ansible — playbook.yml

This playbook creates the entire infrastructure (network, router, security group, data volume, VM, floating IP) and then configures the VM — all in a single run. No Terraform required.

---
- name: Create infrastructure and VM
  hosts: localhost
  vars:
    vm_name: demo-vm
    image: "Ubuntu 24.04"
    flavor: g1.2c4m
    key_name: my-key
    availability_zone: ch-zh1-az2
    volume_size: 100
    volume_type: v-ssd-std
    cidr: 192.168.100.0/24

  tasks:
    - name: Create private network
      openstack.cloud.os_network:
        name: demo-network

    - name: Create subnet
      openstack.cloud.os_subnet:
        name: demo-subnet
        network_name: demo-network
        cidr: "{{ cidr }}"
        dns_nameservers:
          - 8.8.8.8
          - 8.8.4.4

    - name: Create router with external gateway and interface
      openstack.cloud.os_router:
        name: demo-router
        network: public
        interfaces:
          - subnet: demo-subnet

    - name: Create security group
      openstack.cloud.os_security_group:
        name: demo-secgroup
        description: Allow SSH and ICMP

    - name: Allow SSH
      openstack.cloud.os_security_group_rule:
        security_group: demo-secgroup
        protocol: tcp
        port_range_min: 22
        port_range_max: 22
        remote_ip_prefix: 0.0.0.0/0

    - name: Allow ICMP
      openstack.cloud.os_security_group_rule:
        security_group: demo-secgroup
        protocol: icmp
        remote_ip_prefix: 0.0.0.0/0

    - name: Create data volume
      openstack.cloud.os_volume:
        name: demo-data-vol
        size: "{{ volume_size }}"
        volume_type: "{{ volume_type }}"
        availability_zone: "{{ availability_zone }}"

    - name: Launch VM instance
      openstack.cloud.os_server:
        name: "{{ vm_name }}"
        image: "{{ image }}"
        flavor: "{{ flavor }}"
        key_name: "{{ key_name }}"
        security_groups: demo-secgroup
        availability_zone: "{{ availability_zone }}"
        nics:
          - net-name: demo-network
        delete_fip: false
      register: vm

    - name: Attach data volume to VM
      openstack.cloud.os_server_volume:
        server: "{{ vm_name }}"
        volume: demo-data-vol

    - name: Allocate and associate floating IP
      openstack.cloud.os_floating_ip:
        server: "{{ vm_name }}"
        network: public
        wait: true
      register: fip

    - name: Add VM to in-memory inventory
      ansible.builtin.add_host:
        name: demo-vm
        ansible_host: "{{ fip.floating_ip.floating_ip_address }}"
        ansible_user: ubuntu
        ansible_ssh_private_key_file: ~/.ssh/id_rsa

    - name: Wait for SSH to be available
      ansible.builtin.wait_for:
        host: "{{ fip.floating_ip.floating_ip_address }}"
        port: 22
        timeout: 300

    - name: Show floating IP
      ansible.builtin.debug:
        msg: "VM floating IP: {{ fip.floating_ip.floating_ip_address }}"

- name: Configure VM
  hosts: demo-vm
  become: true

  tasks:
    - name: Identify the attached volume
      ansible.builtin.shell: lsblk -o NAME,SIZE -n | awk '$2 ~ /100G/ {print $1; exit}'
      register: volume_device
      changed_when: false

    - name: Create partition on the data volume
      ansible.builtin.shell: |
        parted -s /dev/{{ volume_device.stdout }} mklabel gpt mkpart primary ext4 0% 100%
      when: volume_device.stdout != ""

    - name: Format the partition
      ansible.builtin.filesystem:
        fstype: ext4
        dev: "/dev/{{ volume_device.stdout }}1"
        opts: -L datavol

    - name: Create mount point
      ansible.builtin.file:
        path: /mnt/data
        state: directory
        mode: "0755"

    - name: Mount the volume
      ansible.builtin.mount:
        path: /mnt/data
        src: LABEL=datavol
        fstype: ext4
        opts: noatime,nodiratime,nofail
        state: mounted

    - name: Install Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Ensure Nginx is running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Run

ansible-playbook -i localhost, playbook.yml

The playbook outputs the floating IP at the end of the first play:

TASK [Show floating IP] ****
ok: [localhost] => {
    "msg": "VM floating IP: 185.123.45.67"
}

Note

The first play runs against localhost and talks to the OpenStack API via clouds.yaml. The second play connects to the VM's floating IP via SSH. The add_host task bridges the two plays by adding the VM to the in-memory inventory.

Verify

After deploying with either tool, verify the setup:

Terraform

ssh ubuntu@$(terraform output -raw floating_ip)

Ansible

ssh ubuntu@<floating-ip-from-playbook-output>

On the VM

lsblk
df -h /mnt/data
curl -s http://localhost | head -5

Cleanup

Terraform

terraform destroy

This removes all resources created by Terraform: instance, volume, floating IP, network, router, and security group.

Ansible

Delete resources in reverse order using the OpenStack CLI:

openstack server delete demo-vm
openstack volume delete demo-data-vol
openstack router remove subnet demo-router demo-subnet
openstack router delete demo-router
openstack network delete demo-network
openstack security group delete demo-secgroup

Warning

The floating IP allocated by Ansible is disassociated when the server is deleted, but not released. Find and release it with openstack floating ip list and openstack floating ip release <ADDRESS>.