add hetzner-vm-provision flow and document it in README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 17:59:52 +02:00
parent 3db560ec91
commit 56a5afddc2
2 changed files with 277 additions and 0 deletions
+37
View File
@@ -7,6 +7,7 @@ Aktuell enthalten:
* `docker_stack_inventory_scan.yaml`
* `build_select_options.py`
* `linux_apt-upgrade.yaml`
* `hetzner-vm-provision.yaml`
* `.env.example`
Der erste Flow scannt mehrere Docker-Hosts per SSH, sucht nach Docker-Compose-Stacks und schreibt daraus eine Dropdown-Liste in den Kestra KV-Store.
@@ -101,6 +102,42 @@ SSH_PRIVATE_KEY
---
### `hetzner-vm-provision.yaml`
Kestra-Flow zum Provisionieren einer Hetzner Cloud VM via Terraform — mit KI-gestützter Plan-Analyse und manuellem Approval-Gate.
**Namespace:** `hetzner`
Der Flow läuft in vier Phasen:
1. **Terraform Plan** — erstellt einen Plan für die neue VM (Ubuntu 24.04) im Hetzner-Datacenter; Terraform State wird im S3-Backend (Hetzner Object Storage) gespeichert
2. **LLM-Analyse** — GPT-4o-mini fasst den Plan auf Deutsch zusammen und bewertet ob er sicher aussieht
3. **Approval-Gate** — Kestra pausiert und wartet auf manuelle Freigabe
4. **Terraform Apply / Abbruch** — bei Freigabe wird die VM erstellt und die IPv4-Adresse ausgegeben; bei Ablehnung wird die Execution abgebrochen
Der Flow erwartet folgende Eingaben:
| Input | Typ | Beschreibung |
|---|---|---|
| `vm_name` | STRING | Eindeutiger VM-Name, z.B. `dev-server-01` |
| `server_type` | SELECT | `cx23` (2 vCPU / 4 GB) · `cx33` · `cx43` · `cx53` |
| `location` | SELECT | `nbg1` · `fsn1` · `hel1` |
| `team` | STRING | Label für die VM |
Benötigte Kestra Secrets:
```text
HCLOUD_TOKEN
SSH_KEY_NAME
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
TF_BACKEND_BUCKET
TF_BACKEND_ENDPOINT
OPENAI_API_KEY
```
---
### `.env.example`
Beispiel-Datei für benötigte Umgebungsvariablen und Secrets.
+240
View File
@@ -0,0 +1,240 @@
id: hetzner-vm-provision
namespace: hetzner
inputs:
- id: vm_name
type: STRING
displayName: "VM Name"
description: "Eindeutiger Name der VM, z.B. dev-server-01."
- id: server_type
type: SELECT
displayName: "Server-Typ"
description: "cx23 = 2 vCPU 4 GB ~4 €/Mon | cx33 = 4 vCPU 8 GB ~9 €/Mon | cpx42 = 8 vCPU 16 GB ~30 €/Mon"
values:
- cx23
- cx33
- cx43
- cx53
defaults: cx23
- id: location
type: SELECT
displayName: "Standort"
description: "Hetzner-Datacenter"
values:
- nbg1
- fsn1
- hel1
defaults: nbg1
- id: team
type: STRING
displayName: "Team"
description: "Wird als Label auf der VM gesetzt."
defaults: demo
tasks:
- id: terraform-plan
type: io.kestra.plugin.terraform.cli.TerraformCLI
containerImage: hashicorp/terraform:latest
env:
AWS_ACCESS_KEY_ID: "{{ secret('AWS_ACCESS_KEY_ID') }}"
AWS_SECRET_ACCESS_KEY: "{{ secret('AWS_SECRET_ACCESS_KEY') }}"
TF_BACKEND_BUCKET: "{{ secret('TF_BACKEND_BUCKET') }}"
TF_BACKEND_ENDPOINT: "{{ secret('TF_BACKEND_ENDPOINT') }}"
TF_VAR_hcloud_token: "{{ secret('HCLOUD_TOKEN') }}"
TF_VAR_ssh_key_name: "{{ secret('SSH_KEY_NAME') }}"
inputFiles:
main.tf: |
terraform {
required_version = ">= 1.5"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.49"
}
}
backend "s3" {}
}
provider "hcloud" {
token = var.hcloud_token
}
data "hcloud_ssh_key" "default" {
name = var.ssh_key_name
}
resource "hcloud_server" "vm" {
name = var.vm_name
image = "ubuntu-24.04"
server_type = var.server_type
location = var.location
ssh_keys = [data.hcloud_ssh_key.default.id]
public_net {
ipv4_enabled = true
ipv6_enabled = false
}
labels = {
managed_by = "terraform"
team = var.team
}
}
output "vm_ipv4" {
value = hcloud_server.vm.ipv4_address
}
variables.tf: |
variable "hcloud_token" {
type = string
sensitive = true
}
variable "ssh_key_name" {
type = string
}
variable "vm_name" {
type = string
}
variable "server_type" {
type = string
default = "cx23"
}
variable "location" {
type = string
default = "nbg1"
}
variable "team" {
type = string
default = "demo"
}
beforeCommands:
- |
terraform init -reconfigure \
-backend-config="bucket=$TF_BACKEND_BUCKET" \
-backend-config="endpoints={s3=\"$TF_BACKEND_ENDPOINT\"}" \
-backend-config="key=vms/{{ inputs.vm_name }}/terraform.tfstate" \
-backend-config="region=us-east-1" \
-backend-config="skip_requesting_account_id=true" \
-backend-config="skip_credentials_validation=true" \
-backend-config="skip_metadata_api_check=true" \
-backend-config="skip_region_validation=true" \
-backend-config="use_path_style=true"
commands:
- |
set -eo pipefail
terraform plan -no-color -out=tfplan \
-var="vm_name={{ inputs.vm_name }}" \
-var="server_type={{ inputs.server_type }}" \
-var="location={{ inputs.location }}" \
-var="team={{ inputs.team }}" \
| tee plan-output.txt
outputFiles:
- tfplan
- main.tf
- variables.tf
- plan-output.txt
- id: llm-summary
type: io.kestra.plugin.openai.ChatCompletion
apiKey: "{{ secret('OPENAI_API_KEY') }}"
model: gpt-4o-mini
messages:
- role: system
content: "Du bist ein DevOps-Assistent. Fasse den folgenden Terraform Plan auf Deutsch in 3-5 Sätzen zusammen. Erkläre, was erstellt oder geändert wird und ob der Plan sicher aussieht."
- role: user
content: "{{ read(outputs['terraform-plan'].outputFiles['plan-output.txt']) }}"
- id: log-summary
type: io.kestra.plugin.core.log.Log
message: |
📋 KI-Analyse des Terraform Plans:
{{ outputs['llm-summary'].choices[0].message.content }}
──────────────────────────────────────────
→ Bitte prüfen und Execution fortsetzen oder ablehnen.
- id: approval-gate
type: io.kestra.plugin.core.flow.Pause
onResume:
- id: approved
type: BOOLEAN
displayName: "Deployment genehmigen?"
description: "Ja = VM wird erstellt. Nein = Execution wird abgebrochen."
defaults: false
- id: deploy-decision
type: io.kestra.plugin.core.flow.If
condition: "{{ outputs['approval-gate'].onResume.approved == true }}"
then:
- id: terraform-apply
type: io.kestra.plugin.terraform.cli.TerraformCLI
containerImage: hashicorp/terraform:latest
env:
AWS_ACCESS_KEY_ID: "{{ secret('AWS_ACCESS_KEY_ID') }}"
AWS_SECRET_ACCESS_KEY: "{{ secret('AWS_SECRET_ACCESS_KEY') }}"
TF_BACKEND_BUCKET: "{{ secret('TF_BACKEND_BUCKET') }}"
TF_BACKEND_ENDPOINT: "{{ secret('TF_BACKEND_ENDPOINT') }}"
TF_VAR_hcloud_token: "{{ secret('HCLOUD_TOKEN') }}"
TF_VAR_ssh_key_name: "{{ secret('SSH_KEY_NAME') }}"
inputFiles:
main.tf: "{{ outputs['terraform-plan'].outputFiles['main.tf'] }}"
variables.tf: "{{ outputs['terraform-plan'].outputFiles['variables.tf'] }}"
tfplan: "{{ outputs['terraform-plan'].outputFiles['tfplan'] }}"
beforeCommands:
- |
terraform init -reconfigure \
-backend-config="bucket=$TF_BACKEND_BUCKET" \
-backend-config="endpoints={s3=\"$TF_BACKEND_ENDPOINT\"}" \
-backend-config="key=vms/{{ inputs.vm_name }}/terraform.tfstate" \
-backend-config="region=us-east-1" \
-backend-config="skip_requesting_account_id=true" \
-backend-config="skip_credentials_validation=true" \
-backend-config="skip_metadata_api_check=true" \
-backend-config="skip_region_validation=true" \
-backend-config="use_path_style=true"
commands:
- terraform apply -auto-approve -no-color tfplan
- terraform output -raw vm_ipv4 | tr -d '\n' > vm_ipv4.txt
outputFiles:
- vm_ipv4.txt
- id: log-result
type: io.kestra.plugin.core.log.Log
message: |
✅ VM erfolgreich bereitgestellt!
Name: {{ inputs.vm_name }}
Typ: {{ inputs.server_type }} @ {{ inputs.location }}
Team: {{ inputs.team }}
IPv4: {{ read(outputs['terraform-apply'].outputFiles['vm_ipv4.txt']) }}
SSH: ssh root@{{ read(outputs['terraform-apply'].outputFiles['vm_ipv4.txt']) }}
else:
- id: log-aborted
type: io.kestra.plugin.core.log.Log
message: "❌ Deployment abgebrochen Approval wurde verweigert."