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: 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
}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 applyNach 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 pingAntwortet 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 Passwortmanagerlionbackup.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: trueDer 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.ymlno_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 ./restoreZuerst die Liste. Sie zeigt jedes Backup des Projekts mit File-ID, Name, Größe und Zeitpunkt:
./lionbackup --config restore.yaml --listDann 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 ./restoreDer 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.
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.