Zum Inhalt

Komplettes Setup mit Terraform & Ansible

Dieser Leitfaden bietet zwei vollständige, kopierfähige Infrastructure-as-Code-Beispiele, die eine VM mit Netzwerk, Security Group, Data-Volume und Floating IP bereitstellen — eines mit Terraform, das andere mit Ansible. Beide erstellen dieselbe Architektur; wählen Sie das Werkzeug, das zu Ihrem Workflow passt. Das Ansible-Playbook konfiguriert die VM zusätzlich (formatiert das Data-Volume, installiert Nginx).

Architektur-Übersicht

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

    subgraph Provider ["Provider-Netzwerk"]
        ExtNet["Externes Netzwerk (public)"]
    end

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

        subgraph Net ["Privates Netzwerk"]
            Subnet["Subnetz 192.168.100.0/24"]
            Router <-->|Interface| Subnet

            subgraph SG ["Security Group"]
                VM["VM-Instanz"]
            end
        end

        Vol["Data-Volume (100 GB)"]
        Vol <-.->|Angehängt| 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-Netzwerk"]
        ExtNet["Externes Netzwerk (public)"]
    end

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

        subgraph Net ["Privates Netzwerk"]
            Subnet["Subnetz 192.168.100.0/24"]
            Router <-->|Interface| Subnet

            subgraph SG ["Security Group"]
                VM["VM-Instanz"]
            end
        end

        Vol["Data-Volume (100 GB)"]
        Vol <-.->|Angehängt| 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;

Voraussetzungen

Projekt-Struktur

openstack-demo/
├── main.tf          # Terraform: gesamte Infrastruktur + VM
├── playbook.yml     # Ansible: gesamte Infrastruktur + VM + Konfiguration (eigenständig)
└── clouds.yaml      # Application Credentials (nicht in der Versionsverwaltung)

Eigenständige Ansätze

main.tf und playbook.yml sind unabhängig — Sie können entweder das eine oder das andere verwenden. Sie stellen dieselben Ressourcen bereit.

Terraform — main.tf

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

provider "openstack" {
  cloud = "openstack"
}

# ── Netzwerk ─────────────────────────────────────────────

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 = "SSH und ICMP erlauben"

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

# ── Instanz ──────────────────────────────────────────────

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
}

Bereitstellen

cd openstack-demo
terraform init
terraform plan
terraform apply

Terraform gibt nach einem erfolgreichen Apply die Floating IP aus:

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

Ansible — playbook.yml

Dieses Playbook erstellt die gesamte Infrastruktur (Netzwerk, Router, Security Group, Data-Volume, VM, Floating IP) und konfiguriert anschliessend die VM — alles in einem einzigen Lauf. Terraform wird nicht benötigt.

---
- name: Infrastruktur und VM erstellen
  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: Privates Netzwerk erstellen
      openstack.cloud.os_network:
        name: demo-network

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

    - name: Router mit externem Gateway und Interface erstellen
      openstack.cloud.os_router:
        name: demo-router
        network: public
        interfaces:
          - subnet: demo-subnet

    - name: Security Group erstellen
      openstack.cloud.os_security_group:
        name: demo-secgroup
        description: SSH und ICMP erlauben

    - name: SSH erlauben
      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: ICMP erlauben
      openstack.cloud.os_security_group_rule:
        security_group: demo-secgroup
        protocol: icmp
        remote_ip_prefix: 0.0.0.0/0

    - name: Data-Volume erstellen
      openstack.cloud.os_volume:
        name: demo-data-vol
        size: "{{ volume_size }}"
        volume_type: "{{ volume_type }}"
        availability_zone: "{{ availability_zone }}"

    - name: VM-Instanz starten
      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: Data-Volume an VM anhängen
      openstack.cloud.os_server_volume:
        server: "{{ vm_name }}"
        volume: demo-data-vol

    - name: Floating IP zuweisen und zuordnen
      openstack.cloud.os_floating_ip:
        server: "{{ vm_name }}"
        network: public
        wait: true
      register: fip

    - name: VM zum In-Memory-Inventory hinzufügen
      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: Warten bis SSH verfügbar ist
      ansible.builtin.wait_for:
        host: "{{ fip.floating_ip.floating_ip_address }}"
        port: 22
        timeout: 300

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

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

  tasks:
    - name: Angehängtes Volume identifizieren
      ansible.builtin.shell: lsblk -o NAME,SIZE -n | awk '$2 ~ /100G/ {print $1; exit}'
      register: volume_device
      changed_when: false

    - name: Partition auf dem Data-Volume erstellen
      ansible.builtin.shell: |
        parted -s /dev/{{ volume_device.stdout }} mklabel gpt mkpart primary ext4 0% 100%
      when: volume_device.stdout != ""

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

    - name: Mount-Point erstellen
      ansible.builtin.file:
        path: /mnt/data
        state: directory
        mode: "0755"

    - name: Volume einhängen
      ansible.builtin.mount:
        path: /mnt/data
        src: LABEL=datavol
        fstype: ext4
        opts: noatime,nodiratime,nofail
        state: mounted

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

    - name: Sicherstellen, dass Nginx läuft
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Ausführen

ansible-playbook -i localhost, playbook.yml

Das Playbook gibt am Ende des ersten Plays die Floating IP aus:

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

Note

Der erste Play läuft gegen localhost und kommuniziert über clouds.yaml mit der OpenStack-API. Der zweite Play verbindet sich über SSH mit der Floating IP der VM. Die add_host-Aufgabe verbindet die beiden Plays, indem sie die VM zum In-Memory-Inventory hinzufügt.

Verifizieren

Nach dem Deployment mit einem der beiden Werkzeuge kann das Setup verifiziert werden:

Terraform

ssh ubuntu@$(terraform output -raw floating_ip)

Ansible

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

Auf der VM

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

Aufräumen

Terraform

terraform destroy

Dadurch werden alle von Terraform erstellten Ressourcen entfernt: Instanz, Volume, Floating IP, Netzwerk, Router und Security Group.

Ansible

Ressourcen in umgekehrter Reihenfolge mit dem OpenStack-CLI löschen:

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

Die von Ansible zugewiesene Floating IP wird beim Löschen des Servers disassoziiert, aber nicht freigegeben. Finden und freigeben mit openstack floating ip list und openstack floating ip release <ADRESSE>.