Aller au contenu

Configuration complète avec Terraform & Ansible

Ce guide propose deux exemples complets d'infrastructure-as-code, prêts à copier-coller, qui déploient une VM avec réseau, groupe de sécurité, volume de données et IP flottante : l'un repose sur Terraform, l'autre sur Ansible. Les deux créent la même architecture ; choisissez l'outil qui correspond à votre flux de travail. Le playbook Ansible configure en outre la VM (formatage du volume de données, installation de Nginx).

Vue d'ensemble de l'architecture

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

Prérequis

Structure du projet

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

Approches autonomes

main.tf et playbook.yml sont indépendants : vous pouvez utiliser l'un sans l'autre. Ils déploient les mêmes ressources.

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
}

Déploiement

cd openstack-demo
terraform init
terraform plan
terraform apply

Terraform affiche l'IP flottante après un apply réussi :

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

Ansible — playbook.yml

Ce playbook crée toute l'infrastructure (réseau, routeur, groupe de sécurité, volume de données, VM, IP flottante), puis configure la VM, le tout en une seule exécution. Terraform n'est pas nécessaire.

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

Exécution

ansible-playbook -i localhost, playbook.yml

Le playbook affiche l'IP flottante à la fin du premier play :

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

Note

Le premier play s'exécute sur localhost et communique avec l'API OpenStack via clouds.yaml. Le second play se connecte à l'IP flottante de la VM par SSH. La tâche add_host fait le lien entre les deux plays en ajoutant la VM à l'inventaire en mémoire.

Vérification

Après un déploiement avec l'un ou l'autre des outils, vérifiez l'installation :

Terraform

ssh ubuntu@$(terraform output -raw floating_ip)

Ansible

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

Sur la VM

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

Nettoyage

Terraform

terraform destroy

Cette commande supprime toutes les ressources créées par Terraform : instance, volume, IP flottante, réseau, routeur et groupe de sécurité.

Ansible

Supprimez les ressources dans l'ordre inverse avec 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 allouée par Ansible est dissociée lorsque le serveur est supprimé, mais n'est pas libérée. Retrouvez-la et libérez-la avec openstack floating ip list et openstack floating ip release <ADDRESS>.