Vai al contenuto

Configurazione completa con Terraform & Ansible

Questa guida propone due esempi completi di infrastructure-as-code, pronti da copiare e incollare, che distribuiscono una VM con rete, gruppo di sicurezza, volume dati e IP flottante: uno usa Terraform, l'altro Ansible. Entrambi creano la stessa architettura; è possibile scegliere lo strumento più adatto al proprio flusso di lavoro. Il playbook Ansible configura inoltre la VM (formatta il volume dati, installa Nginx).

Panoramica dell'architettura

%%{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;

Prerequisiti

Struttura del progetto

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

Approcci autonomi

main.tf e playbook.yml sono indipendenti: è possibile usare l'uno senza l'altro. Distribuiscono le stesse risorse.

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
}

Distribuzione

cd openstack-demo
terraform init
terraform plan
terraform apply

Terraform mostra l'IP flottante al termine di un apply riuscito:

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

Ansible — playbook.yml

Questo playbook crea l'intera infrastruttura (rete, router, gruppo di sicurezza, volume dati, VM, IP flottante) e poi configura la VM, il tutto in un'unica esecuzione. Terraform non è necessario.

---
- 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

Esecuzione

ansible-playbook -i localhost, playbook.yml

Il playbook mostra l'IP flottante al termine del primo play:

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

Note

Il primo play viene eseguito su localhost e comunica con l'API OpenStack tramite clouds.yaml. Il secondo play si connette all'IP flottante della VM via SSH. L'attività add_host collega i due play aggiungendo la VM all'inventario in memoria.

Verifica

Dopo la distribuzione con uno dei due strumenti, verifichi la configurazione:

Terraform

ssh ubuntu@$(terraform output -raw floating_ip)

Ansible

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

Sulla VM

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

Pulizia

Terraform

terraform destroy

Questo comando rimuove tutte le risorse create da Terraform: istanza, volume, IP flottante, rete, router e gruppo di sicurezza.

Ansible

Elimini le risorse in ordine inverso con la CLI OpenStack:

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

L'IP flottante allocata da Ansible viene dissociata quando il server viene eliminato, ma non viene rilasciata. È possibile trovarla e rilasciarla con openstack floating ip list e openstack floating ip release <ADDRESS>.