L lionbackup CLOUD
REGISTRIEREN LOGIN

STACKIT-VM mit Terraform, Ansible und lionbackup: /data sichern und wiederherstellen

Veröffentlicht am 25. September 2026 · 8 Schritte · ← Blog

Terraform legt die VM in der STACKIT Cloud und das Projekt bei lionbackup in einem Durchgang an, Ansible richtet den Client ein, und /data wird jede Nacht verschlüsselt gesichert. Mit vollständigen HCL- und Playbook-Beispielen zum Kopieren, und mit dem Restore auf eine neue Maschine am Ende.

Schritt 1 / 8

Schritt 1: Voraussetzungen und Zugangsdaten

Am Ende dieser Anleitung läuft eine VM in der STACKIT Cloud, die jede Nacht ihr Verzeichnis /data verschlüsselt nach lionbackup sichert. Alles, was dafür in der Cloud entstehen muss, beschreibt eine einzige Terraform-Konfiguration: Netz, Firewall, Schlüsselpaar und Server bei STACKIT, Projekt und Token bei lionbackup. Ansible installiert danach den Client und richtet den Zeitplan ein. Zum Schluss spielen wir den Ernstfall durch und holen die Daten auf eine neue Maschine zurück.

Sie brauchen vorab:

  • ein STACKIT-Projekt mit einem Service Account und dessen Schlüsseldatei (STACKIT-Portal: Service Accounts, Schlüssel erzeugen, JSON herunterladen);
  • ein lionbackup-Konto mit einer Organisation und einem Dienstkonto samt API-Schlüssel (Portal: Developer). Das Dienstkonto darf genau das, was sein Besitzer darf; legen Sie es also mit einem Benutzer an, der nur diese Organisation besitzt;
  • lokal Terraform 1.9 oder neuer (OpenTofu funktioniert ebenso), Ansible und einen SSH-Schlüssel (~/.ssh/id_ed25519.pub).

Der STACKIT-Provider kommt aus der Terraform-Registry. Unser Provider liegt auf unserem eigenen Mirror. Damit eine vorhandene ~/.terraformrc unangetastet bleibt, bekommt das Projektverzeichnis eine eigene CLI-Konfiguration, auf die TF_CLI_CONFIG_FILE zeigt:

mkdir -p ~/stackit-lionbackup && cd ~/stackit-lionbackup
cat > lionbackup.tfrc <<'EOF'
provider_installation {
  network_mirror {
    url     = "https://git.prod.lionbackup.cloud/terraform/providers/"
    include = ["git.lionbackup.cloud/*/*"]
  }
  direct {
    exclude = ["git.lionbackup.cloud/*/*"]
  }
}
EOF
export TF_CLI_CONFIG_FILE="$PWD/lionbackup.tfrc"

Alle anderen Provider bezieht Terraform weiterhin direkt aus der Registry; der Eintrag lenkt nur unseren Namensraum um. Wer seine ~/.terraformrc ohnehin pflegt, kann den Block stattdessen dort ergänzen.

Schritt 2: Beide Provider in einer Konfiguration

Im Projektverzeichnis aus Schritt 1 entsteht die Datei providers.tf. Der STACKIT-Provider bekommt die Schlüsseldatei des Service Accounts, unser Provider liest den API-Schlüssel aus der Umgebung. Ein Schlüssel in der Konfiguration landet früher oder später in der Versionsverwaltung, deshalb steht er dort nicht.

terraform {
  required_version = ">= 1.9"
  required_providers {
    stackit = {
      source  = "stackitcloud/stackit"
      version = "~> 0.116"
    }
    lionbackup = {
      source  = "git.lionbackup.cloud/lionbackup/lionbackup"
      version = "~> 0.1.1"
    }
  }
}

provider "stackit" {
  default_region           = "eu01"
  service_account_key_path = pathexpand("~/.stackit/sa-key.json")
}

provider "lionbackup" {
  # api_key kommt aus der Umgebungsvariable LIONBACKUP_API_KEY
}

Vier Werte sind je nach Umgebung verschieden und wandern in variables.tf: die STACKIT-Projekt-ID, die ID des Betriebssystem-Images, das Netz, aus dem SSH erlaubt sein soll, und der Name Ihrer lionbackup-Organisation.

variable "stackit_project_id" {
  type = string
}

variable "image_id" {
  type        = string
  description = "Image-ID eines Debian- oder Ubuntu-Images, siehe: stackit image list --project-id <ID> --all"
}

variable "admin_cidr" {
  type        = string
  description = "Netz, aus dem SSH auf die VM erlaubt ist, z. B. 203.0.113.10/32"
}

variable "organization_name" {
  type        = string
  description = "Name Ihrer Organisation im lionbackup-Portal"
}

Die Image-ID liefert die STACKIT-CLI mit stackit image list --project-id <ID> --all; alternativ steht sie im STACKIT-Portal beim jeweiligen Image.

Schritt 3: Die VM in der STACKIT Cloud

Die VM braucht ein Netz, eine Sicherheitsgruppe mit einer SSH-Regel, das SSH-Schlüsselpaar, eine Netzwerkschnittstelle und eine öffentliche IP. Alles zusammen steht in vm.tf:

resource "stackit_network" "app" {
  project_id = var.stackit_project_id
  name       = "app-net"
}

resource "stackit_security_group" "app" {
  project_id = var.stackit_project_id
  name       = "app-sg"
  stateful   = true
}

# SSH nur aus dem eigenen Netz. Alles andere bleibt zu; der Client
# baut ausgehende Verbindungen auf und braucht keinen offenen Port.
resource "stackit_security_group_rule" "ssh" {
  project_id        = var.stackit_project_id
  security_group_id = stackit_security_group.app.security_group_id
  direction         = "ingress"
  ether_type        = "IPv4"
  ip_range          = var.admin_cidr
  protocol   = { name = "tcp" }
  port_range = { min = 22, max = 22 }
  description = "SSH aus dem Admin-Netz"
}

resource "stackit_key_pair" "admin" {
  name       = "admin"
  public_key = chomp(file(pathexpand("~/.ssh/id_ed25519.pub")))
}

resource "stackit_network_interface" "app" {
  project_id         = var.stackit_project_id
  network_id         = stackit_network.app.network_id
  security_group_ids = [stackit_security_group.app.security_group_id]
}

resource "stackit_public_ip" "app" {
  project_id           = var.stackit_project_id
  network_interface_id = stackit_network_interface.app.network_interface_id
}

resource "stackit_server" "app" {
  project_id        = var.stackit_project_id
  name              = "app-1"
  machine_type      = "g2i.1"
  availability_zone = "eu01-1"
  keypair_name      = stackit_key_pair.admin.name
  boot_volume = {
    size        = 32
    source_type = "image"
    source_id   = var.image_id
  }
  network_interfaces = [stackit_network_interface.app.network_interface_id]
}

g2i.1 ist die kleinste Maschinenklasse; für den Client reicht sie, er hält keine Kopie der Daten im Speicher. Was er braucht, ist Platz im Arbeitsverzeichnis, dazu gleich mehr.

Schritt 4: Projekt und Token bei lionbackup

Auf der lionbackup-Seite entstehen drei Dinge: ein Projekt in der Storage-Zone de01-1, ein Write-Token für die VM und ein Read-Token für den Restore. Die Trennung ist der Kern des Modells: Das Token auf der VM kann ausschließlich sichern. Wer die VM übernimmt, bekommt damit weder Liste noch Inhalt der Backups.

Die Organisation wird über ihren Namen gewählt, nicht über die Position in der Liste. Ein Dienstkonto kann mehrere Organisationen sehen, und one() bricht bei mehreren Treffern ab; gibt es keinen, bleibt organization_id leer und Terraform verweigert den Plan. Besser ein Abbruch als ein Projekt in der falschen Organisation.

data "lionbackup_organizations" "mine" {}

locals {
  organization_id = one([
    for o in data.lionbackup_organizations.mine.organizations : o.id
    if o.name == var.organization_name
  ])
}

resource "lionbackup_project" "app" {
  organization_id   = local.organization_id
  name              = "stackit-app-1"
  availability_zone = "de01-1"
  alert_email       = "ops@example.com"
}

resource "lionbackup_project_token" "writer" {
  project_id = lionbackup_project.app.id
  type       = "write"
}

resource "lionbackup_project_token" "reader" {
  project_id = lionbackup_project.app.id
  type       = "read"
}

Die Ausgaben in outputs.tf geben nach dem Apply die IP der VM und die Projekt-ID her; die beiden Token-Geheimnisse bleiben als sensitive markiert und erscheinen nur auf ausdrückliche Nachfrage.

output "public_ip" {
  value = stackit_public_ip.app.ip
}

output "lionbackup_project_id" {
  value = lionbackup_project.app.id
}

output "backup_token" {
  value     = lionbackup_project_token.writer.secret
  sensitive = true
}

output "restore_token" {
  value     = lionbackup_project_token.reader.secret
  sensitive = true
}
Der Terraform-State enthält beide Token. Behandeln Sie ihn wie ein Passwort: verschlüsseltes Remote-Backend, keine Kopie im Git. Ein Token lässt sich im Portal jederzeit widerrufen; Terraform legt beim nächsten Apply ein neues an.

Schritt 5: Ausrollen

Den API-Schlüssel des lionbackup-Dienstkontos setzen Sie als Umgebungsvariable, dann wie gewohnt initialisieren und anwenden. Beim init zieht Terraform unseren Provider vom Mirror und prüft seine Prüfsumme.

export TF_CLI_CONFIG_FILE="$PWD/lionbackup.tfrc"
export LIONBACKUP_API_KEY='<Schlüssel des Dienstkontos>'
export TF_VAR_stackit_project_id='<STACKIT-Projekt-ID>'
export TF_VAR_image_id='<Image-ID>'
export TF_VAR_admin_cidr='203.0.113.10/32'
export TF_VAR_organization_name='Meine Firma GmbH'

terraform init
terraform apply

Nach dem Apply stehen zehn Ressourcen: sieben bei STACKIT, drei bei lionbackup, dazu die Ausgaben. Für Ansible reicht ein Inventar mit der IP; den Standardbenutzer gibt das Image vor (bei den Debian-Images debian, bei Ubuntu ubuntu).

cat > inventory.ini <<EOF
[app]
$(terraform output -raw public_ip) ansible_user=debian
EOF

ansible -i inventory.ini app -m ping

Antwortet die VM mit pong, ist der Weg frei. Falls nicht: Die SSH-Regel lässt nur admin_cidr herein, prüfen Sie zuerst Ihre eigene Adresse.

Schritt 6: Schlüsselpaar und Ansible-Playbook

Die Verschlüsselung passiert auf der VM, bevor ein Byte das Rechenzentrum verlässt. Dafür braucht der Client ein Schlüsselpaar, und das erzeugen Sie auf Ihrem Arbeitsplatz, nicht auf der VM: Nur der öffentliche Teil wandert auf den Server. Der private Schlüssel entschlüsselt später das Backup und gehört an einen Ort, der einen Ausfall der VM überlebt.

curl -fsSLo lionbackup https://downloads.lionbackup.cloud/lionbackup-linux-amd64
chmod +x lionbackup
./lionbackup --generate-key --key-name ./lionbackup
# lionbackup.pub  -> geht mit Ansible auf die VM
# lionbackup.key  -> offline aufbewahren, z. B. im Passwortmanager
Ohne lionbackup.key gibt es kein Restore. Wir speichern den privaten Schlüssel nirgends und können ihn nicht wiederherstellen. Verwendet wird ein Post-Quanten-Hybridverfahren aus ML-KEM-768 und X25519.

Das Playbook backup.yml lädt den Client von unserem Download-Server, legt Konfiguration und Schlüssel ab und richtet einen systemd-Timer ein. Projekt-ID und Write-Token kommen aus den Terraform-Ausgaben, über Umgebungsvariablen statt über Dateien.

- name: lionbackup auf der STACKIT-VM einrichten
  hosts: app
  become: true
  vars:
    lionbackup_project: "{{ lookup('env', 'LB_PROJECT') }}"
    lionbackup_token: "{{ lookup('env', 'LB_TOKEN') }}"
    lionbackup_zone: de01-1
  tasks:
    - name: Client herunterladen
      ansible.builtin.get_url:
        url: https://downloads.lionbackup.cloud/lionbackup-linux-amd64
        dest: /usr/local/bin/lionbackup
        mode: "0755"

    - name: Verzeichnisse anlegen
      ansible.builtin.file:
        path: "{{ item }}"
        state: directory
        mode: "0750"
      loop:
        - /etc/lionbackup
        - /var/lib/lionbackup/tmp
        - /data

    - name: Oeffentlichen Schluessel ablegen
      ansible.builtin.copy:
        src: lionbackup.pub
        dest: /etc/lionbackup/lionbackup.pub
        mode: "0644"

    # Der Client akzeptiert eine Konfiguration mit Token nur, wenn
    # ausschliesslich ihr Eigentuemer sie lesen kann: root, 0600.
    - name: Konfiguration schreiben
      ansible.builtin.copy:
        dest: /etc/lionbackup/backup.yaml
        mode: "0600"
        content: |
          lionbackup:
            project: {{ lionbackup_project }}
            storagezone: {{ lionbackup_zone }}
            token: {{ lionbackup_token }}
            files:
              - /data
            compression_method: ZSTD
            compression_level: 5
            encryption_keyfile: /etc/lionbackup/lionbackup.pub
            tmp_directory: /var/lib/lionbackup/tmp
            chunksize: 64
            log_level: INFO
      no_log: true

    - name: systemd-Dienst
      ansible.builtin.copy:
        dest: /etc/systemd/system/lionbackup.service
        mode: "0644"
        content: |
          [Unit]
          Description=lionbackup - Sicherung von /data

          [Service]
          Type=oneshot
          ExecStart=/usr/local/bin/lionbackup --config /etc/lionbackup/backup.yaml

    - name: systemd-Timer
      ansible.builtin.copy:
        dest: /etc/systemd/system/lionbackup.timer
        mode: "0644"
        content: |
          [Unit]
          Description=lionbackup taeglich um 01:00

          [Timer]
          OnCalendar=*-*-* 01:00:00
          RandomizedDelaySec=15m
          Persistent=true

          [Install]
          WantedBy=timers.target

    - name: Timer aktivieren
      ansible.builtin.systemd_service:
        name: lionbackup.timer
        enabled: true
        state: started
        daemon_reload: true

Der Arbeitsbereich /var/lib/lionbackup/tmp liegt auf der 32-GB-Bootplatte. Dort packt und verschlüsselt der Client, bevor er hochlädt; planen Sie etwa das 1,2-fache des größten Backups ein. Wächst /data darüber hinaus, hängen Sie ein STACKIT-Volume ein oder vergrößern die Bootplatte in vm.tf.

export LB_PROJECT=$(terraform output -raw lionbackup_project_id)
export LB_TOKEN=$(terraform output -raw backup_token)
ansible-playbook -i inventory.ini backup.yml

no_log: true an der Konfigurationsaufgabe sorgt dafür, dass das Token nicht im Ansible-Protokoll auftaucht, auch nicht bei -vvv.

Schritt 7: Erstes Backup

Der Timer wartet bis 01:00 Uhr. Das erste Backup stoßen Sie von Hand an und sehen dabei zu:

ssh debian@$(terraform output -raw public_ip) \
  'sudo systemctl start lionbackup.service && sudo journalctl -u lionbackup.service -n 30 --no-pager'

Der Client meldet drei Phasen: Quellen prüfen, dann archivieren, komprimieren und verschlüsseln, dann hochladen; bei größeren Mengen alle zwei Sekunden eine Fortschrittszeile. Am Ende druckt er die File-ID des eben erstellten Backups. Sie erscheint zugleich im Portal unter „Letzte Backups“ des Projekts.

Ab jetzt läuft die Sicherung jede Nacht. Persistent=true holt einen verpassten Lauf nach dem nächsten Start nach, RandomizedDelaySec streut den Start um bis zu 15 Minuten, damit mehrere VMs nicht gleichzeitig loslegen.

Schritt 8: Restore: die Daten auf eine neue Maschine holen

Nehmen wir an, die VM ist weg, samt Bootplatte und allem, was darauf lag. Was Sie noch haben: den Terraform-State mit dem Read-Token, den privaten Schlüssel aus Schritt 6 und die Projekt-ID. Mehr braucht ein Restore nicht, und er läuft auf jeder Linux-Maschine, auf Ihrem Arbeitsplatz genauso wie auf einer frisch angelegten VM.

Die Restore-Konfiguration ist kürzer als die für das Backup: kein files, kein öffentlicher Schlüssel. Das Read-Token kommt aus dem Terraform-Output.

cat > restore.yaml <<EOF
lionbackup:
  project: $(terraform output -raw lionbackup_project_id)
  storagezone: de01-1
  token: $(terraform output -raw restore_token)
  tmp_directory: /var/tmp/lionbackup
EOF
chmod 600 restore.yaml
mkdir -p /var/tmp/lionbackup ./restore

Zuerst die Liste. Sie zeigt jedes Backup des Projekts mit File-ID, Name, Größe und Zeitpunkt:

./lionbackup --config restore.yaml --list

Dann das Backup Ihrer Wahl, entschlüsselt mit dem privaten Schlüssel, entpackt nach ./restore:

./lionbackup --config restore.yaml \
  --restore <FILE-ID> --identity ./lionbackup.key \
  --target ./restore --progress-bar

ls -la ./restore

Der Inhalt von /data liegt jetzt unter ./restore; ein rsync auf die neue VM schließt den Kreis. Der private Schlüssel verlässt dabei die Maschine nicht, auf der Sie den Restore ausführen.

Vorsicht bei terraform destroy. Ein Destroy schließt das Projekt und widerruft dabei jedes Token, das diese Konfiguration verwaltet, auch das Read-Token. Wer die STACKIT-Ressourcen abbauen und die Daten danach noch zurückholen will, sichert vorher das Token (terraform output -raw restore_token) und nimmt es aus dem State: terraform state rm lionbackup_project_token.reader. Dann schließt der Destroy das Projekt, und das Token bleibt gültig, solange Backups in ihrer Vorhaltezeit liegen. Alternativ legen Sie im Portal ein neues Read-Token an; das geht auch nach dem Schließen, solange noch ein Backup in der Vorhaltezeit liegt.

Wer den Restore lieber als Teil der Infrastruktur beschreibt: Eine zweite Terraform-Konfiguration mit einer neuen VM, deren Ansible-Playbook nur restore.yaml und den Aufruf oben enthält, ist wenige Zeilen entfernt. Getestet gehört der Weg zurück so oder so regelmäßig, nicht erst, wenn er gebraucht wird.