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
- Terraform installed and configured — see Terraform Installation
- Ansible installed and configured — see Ansible Installation
- Application Credentials (
clouds.yaml) — see Application Credentials - An SSH key pair uploaded to OpenStack — see SSH Key Pairs
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>.