Deployment: Hetzner Cloud via Terraform (Multi-Node, skalierbar)
Infrastruktur als Code für den Skalierungs-Test auf Hetzner: - deploy/terraform: privates Netz, Firewalls, 2 App-Nodes, DB-Node, Load Balancer (Health-Check /health); cloud-init bootet Docker + Stack je Node - deploy/compose/docker-compose.prod.yml + nginx.prod.conf: App-Node-Stack (PHP-FPM + Nginx) routet /api,/p,/t,/css,/health → Symfony, Rest → Vue-SPA - App-Anpassungen: HealthController (/health für LB), brand.css nach /css verschoben (kein Pfad-Clash mit SPA-Assets im Prod-Routing) - deploy/README.md: Anleitung inkl. JWT-Key-Verteilung & Cross-Node-Test - reference.php (auto-generiert) aus Versionierung entfernt Terraform validiert (terraform validate), Prod-Compose-Syntax geprüft. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
67e4353c8d
commit
c3e05257cb
7
.gitignore
vendored
7
.gitignore
vendored
@ -24,3 +24,10 @@ backend/.playwright-mcp/
|
||||
|
||||
# Auto-generierte Symfony-Config-Referenz
|
||||
/backend/config/reference.php
|
||||
|
||||
# Terraform
|
||||
deploy/terraform/.terraform/
|
||||
deploy/terraform/*.tfstate
|
||||
deploy/terraform/*.tfstate.*
|
||||
deploy/terraform/terraform.tfvars
|
||||
deploy/terraform/.terraform.lock.hcl
|
||||
|
||||
@ -16,6 +16,7 @@ Konzept & Datenmodell: siehe [`docs/KONZEPT.md`](docs/KONZEPT.md).
|
||||
backend/ Symfony-API (JSON/JSON-LD)
|
||||
frontend/ Vue-3-SPA (Dashboards)
|
||||
docker/ Dockerfile (PHP) + Nginx-Config
|
||||
deploy/ Hetzner-Deployment (Terraform + Prod-Compose) — siehe deploy/README.md
|
||||
docs/ Konzept & Datenmodell
|
||||
```
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
29
backend/src/Controller/HealthController.php
Normal file
29
backend/src/Controller/HealthController.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* Health-Check für den Load Balancer. Öffentlich (nicht unter /api),
|
||||
* liefert 200, solange App + DB erreichbar sind, sonst 503.
|
||||
*/
|
||||
final class HealthController
|
||||
{
|
||||
#[Route('/health', name: 'health', methods: ['GET'])]
|
||||
public function __invoke(Connection $connection): JsonResponse
|
||||
{
|
||||
try {
|
||||
$connection->executeQuery('SELECT 1');
|
||||
$db = 'up';
|
||||
$status = 200;
|
||||
} catch (\Throwable) {
|
||||
$db = 'down';
|
||||
$status = 503;
|
||||
}
|
||||
|
||||
return new JsonResponse(['status' => 200 === $status ? 'ok' : 'degraded', 'db' => $db], $status);
|
||||
}
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}vcard4reseller{% endblock %}</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><rect width=%22128%22 height=%22128%22 rx=%2228%22 fill=%22%23f58220%22/><text y=%221.0em%22 x=%220.5em%22 text-anchor=%22middle%22 font-size=%2284%22 font-family=%22Arial%22 font-weight=%22700%22 fill=%22%23fff%22>v</text></svg>">
|
||||
<link rel="stylesheet" href="{{ asset('assets/brand.css') }}">
|
||||
<link rel="stylesheet" href="{{ asset('css/brand.css') }}">
|
||||
{% block stylesheets %}{% endblock %}
|
||||
{% block meta %}{% endblock %}
|
||||
</head>
|
||||
|
||||
76
deploy/README.md
Normal file
76
deploy/README.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Deployment auf Hetzner Cloud (Terraform)
|
||||
|
||||
Provisioniert einen **Multi-Node-Test** mit nachgewiesener Skalierbarkeit:
|
||||
|
||||
```
|
||||
Hetzner Load Balancer (lb11, :80 → /health)
|
||||
/ \
|
||||
vcard4-app-1 vcard4-app-2 (zustandslose App-Nodes)
|
||||
\ /
|
||||
┌─────────┴───────────┐ ┌─────┴──────────────────┐
|
||||
vcard4-db (MariaDB, Volume) Hetzner Object Storage (S3, Assets)
|
||||
```
|
||||
|
||||
App-Nodes sind zustandslos (Code + Docker), **State** liegt in DB (eigene VM) und
|
||||
Object Storage (Hintergrund-PDFs/Schriften). Dadurch beliebig horizontal skalierbar.
|
||||
|
||||
## Voraussetzungen (einmalig, manuell)
|
||||
|
||||
1. **Terraform** ≥ 1.6 + Hetzner **API-Token** (Projekt → Security → API Tokens, Read&Write).
|
||||
2. **Object Storage** in der Hetzner Console anlegen: Bucket + Access-Key/Secret
|
||||
(hcloud/Terraform verwalten Object Storage derzeit nicht).
|
||||
3. **Git-Repo** erreichbar für die Nodes (öffentlich oder Deploy-Token in der `repo_url`).
|
||||
4. **JWT-Schlüsselpaar** einmal erzeugen — auf **allen** Nodes identisch:
|
||||
```bash
|
||||
# lokal im backend/-Container oder mit openssl
|
||||
docker compose exec php php bin/console lexik:jwt:generate-keypair --overwrite
|
||||
# → backend/config/jwt/private.pem & public.pem + Passphrase aus .env (JWT_PASSPHRASE)
|
||||
```
|
||||
Inhalt der beiden PEM-Dateien + Passphrase in `terraform.tfvars` eintragen.
|
||||
5. **DNS**: nach `terraform apply` einen A-Record `domain → load_balancer_ip` setzen
|
||||
(Hetzner DNS ist separat; A-Record auch für die Custom-Domain-Funktion, KONZEPT §11).
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
cd deploy/terraform
|
||||
cp terraform.tfvars.example terraform.tfvars # ausfüllen
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
terraform output # load_balancer_ip etc.
|
||||
```
|
||||
|
||||
cloud-init installiert auf jedem App-Node Docker, klont das Repo, schreibt
|
||||
`.env.prod.local` + JWT-Keys, baut das SPA und startet
|
||||
`deploy/compose/docker-compose.prod.yml`. Migrationen laufen **nur** auf `app-1`.
|
||||
Deploy-Log auf dem Node: `/var/log/vcard4-deploy.log`.
|
||||
|
||||
## Skalierbarkeit verifizieren
|
||||
|
||||
```bash
|
||||
# 1) Login + Token holen (LB-IP oder Domain)
|
||||
TOKEN=$(curl -s -X POST http://<LB-IP>/api/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"admin@vcard4reseller.de","password":"admin"}' | jq -r .token)
|
||||
|
||||
# 2) Health über den LB (verteilt auf beide Nodes)
|
||||
for i in $(seq 1 6); do curl -s http://<LB-IP>/health; echo; done
|
||||
|
||||
# 3) Cross-Node-Beweis: Hintergrund über den LB hochladen, dann
|
||||
# auf BEIDEN Nodes rendern (per SSH) – identische PDFs aus Object Storage:
|
||||
ssh root@<app-1-ip> 'cd /opt/vcard4 && docker compose -f deploy/compose/docker-compose.prod.yml exec -T php php bin/console app:render-card erika-mustermann'
|
||||
ssh root@<app-2-ip> 'cd /opt/vcard4 && docker compose -f deploy/compose/docker-compose.prod.yml exec -T php php bin/console app:render-card erika-mustermann'
|
||||
```
|
||||
|
||||
Horizontal skalieren: `app_count` erhöhen → `terraform apply` (LB-Targets werden automatisch ergänzt).
|
||||
|
||||
## Noch offen / Hinweise
|
||||
|
||||
- **TLS**: aktuell HTTP am LB. Für HTTPS entweder Hetzner **Managed Certificate**
|
||||
am LB (DNS bei Hetzner) oder **Caddy** auf den Nodes (On-Demand-TLS) — letzteres
|
||||
ist auch der Weg für die **Custom-Domains** der Firmenkunden (§11).
|
||||
- **Trusted Proxies**: für korrekte absolute URLs hinter dem LB
|
||||
`framework.trusted_proxies` auf `%env(TRUSTED_PROXIES)%` setzen.
|
||||
- **Seed**: optional einmalig `app:seed` auf `app-1` für Demo-Daten.
|
||||
- **Updates**: neuen Stand ausrollen = auf den App-Nodes `git pull` + `docker compose ... up -d --build` (später per CI/Skript).
|
||||
25
deploy/compose/docker-compose.prod.yml
Normal file
25
deploy/compose/docker-compose.prod.yml
Normal file
@ -0,0 +1,25 @@
|
||||
# Läuft auf jedem App-Node (zustandslos). DB & Object Storage sind extern.
|
||||
# Aufruf mit --project-directory <repo> (Pfade relativ zum Repo-Root):
|
||||
# docker compose --project-directory /opt/vcard4 \
|
||||
# -f deploy/compose/docker-compose.prod.yml up -d --build
|
||||
services:
|
||||
php:
|
||||
build:
|
||||
context: ./docker/php
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
env_file:
|
||||
- ./backend/.env.prod.local
|
||||
restart: unless-stopped
|
||||
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./backend:/app:ro # Symfony public (API + öffentliche Seiten)
|
||||
- ./frontend/dist:/spa:ro # gebautes Vue-SPA (Dashboard)
|
||||
- ./deploy/compose/nginx.prod.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- php
|
||||
restart: unless-stopped
|
||||
39
deploy/compose/nginx.prod.conf
Normal file
39
deploy/compose/nginx.prod.conf
Normal file
@ -0,0 +1,39 @@
|
||||
# Routing auf dem App-Node:
|
||||
# /api, /p, /t, /css, /bundles, /health → Symfony (PHP-FPM)
|
||||
# alles andere → Vue-SPA (history-fallback)
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Standard: SPA-Build
|
||||
root /spa;
|
||||
index index.html;
|
||||
|
||||
client_max_body_size 32m;
|
||||
|
||||
# Symfony-Pfade (API + serverseitige öffentliche Seiten)
|
||||
location ~ ^/(api|p|t|css|bundles|health)(/|$) {
|
||||
root /app/public;
|
||||
try_files $uri /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ ^/index\.php(/|$) {
|
||||
root /app/public;
|
||||
fastcgi_pass php:9000;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||
include fastcgi_params;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param DOCUMENT_ROOT $document_root;
|
||||
internal;
|
||||
}
|
||||
|
||||
# SPA-Routing (history mode)
|
||||
location / {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
# direkte .php-Aufrufe blockieren
|
||||
location ~ \.php$ {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
64
deploy/terraform/cloud-init-app.yaml.tftpl
Normal file
64
deploy/terraform/cloud-init-app.yaml.tftpl
Normal file
@ -0,0 +1,64 @@
|
||||
#cloud-config
|
||||
package_update: true
|
||||
write_files:
|
||||
- path: /opt/secrets/.env.prod.local
|
||||
permissions: '0600'
|
||||
content: |
|
||||
APP_ENV=prod
|
||||
APP_DEBUG=0
|
||||
APP_SECRET=${app_secret}
|
||||
DATABASE_URL="${database_url}"
|
||||
CORS_ALLOW_ORIGIN=${cors_allow_origin}
|
||||
TRUSTED_PROXIES=10.0.0.0/16
|
||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||
JWT_PASSPHRASE=${jwt_passphrase}
|
||||
S3_ENDPOINT=${s3_endpoint}
|
||||
S3_REGION=${s3_region}
|
||||
S3_BUCKET=${s3_bucket}
|
||||
S3_KEY=${s3_key}
|
||||
S3_SECRET=${s3_secret}
|
||||
S3_PATH_STYLE=true
|
||||
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||
- path: /opt/secrets/private.pem.b64
|
||||
permissions: '0600'
|
||||
content: ${base64encode(jwt_private_key)}
|
||||
- path: /opt/secrets/public.pem.b64
|
||||
permissions: '0644'
|
||||
content: ${base64encode(jwt_public_key)}
|
||||
- path: /opt/secrets/deploy.vars
|
||||
permissions: '0600'
|
||||
content: |
|
||||
REPO_URL=${repo_url}
|
||||
REPO_BRANCH=${repo_branch}
|
||||
DOMAIN=${domain}
|
||||
RUN_MIGRATIONS=${run_migrations}
|
||||
- path: /opt/deploy.sh
|
||||
permissions: '0755'
|
||||
content: |
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
. /opt/secrets/deploy.vars
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sh
|
||||
apt-get update && apt-get install -y git
|
||||
rm -rf /opt/vcard4
|
||||
git clone --branch "$REPO_BRANCH" --depth 1 "$REPO_URL" /opt/vcard4
|
||||
cd /opt/vcard4
|
||||
cp /opt/secrets/.env.prod.local backend/.env.prod.local
|
||||
mkdir -p backend/config/jwt
|
||||
base64 -d /opt/secrets/private.pem.b64 > backend/config/jwt/private.pem
|
||||
base64 -d /opt/secrets/public.pem.b64 > backend/config/jwt/public.pem
|
||||
chmod 640 backend/config/jwt/private.pem
|
||||
# SPA bauen (Profil-/QR-Links zeigen auf die öffentliche Domain)
|
||||
docker run --rm -e VITE_PUBLIC_BASE="https://$DOMAIN" -v "$PWD/frontend":/app -w /app node:25-alpine sh -c "npm ci && npm run build"
|
||||
chown -R 1000:1000 /opt/vcard4
|
||||
COMPOSE="docker compose --project-directory /opt/vcard4 -f deploy/compose/docker-compose.prod.yml"
|
||||
$COMPOSE up -d --build
|
||||
sleep 20
|
||||
if [ "$RUN_MIGRATIONS" = "true" ]; then
|
||||
$COMPOSE exec -T php php bin/console doctrine:migrations:migrate --no-interaction || true
|
||||
fi
|
||||
$COMPOSE exec -T php php bin/console cache:clear || true
|
||||
runcmd:
|
||||
- bash /opt/deploy.sh > /var/log/vcard4-deploy.log 2>&1
|
||||
14
deploy/terraform/cloud-init-db.yaml.tftpl
Normal file
14
deploy/terraform/cloud-init-db.yaml.tftpl
Normal file
@ -0,0 +1,14 @@
|
||||
#cloud-config
|
||||
package_update: true
|
||||
runcmd:
|
||||
- command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sh
|
||||
- mkdir -p /opt/db/data
|
||||
- |
|
||||
docker run -d --name mariadb --restart unless-stopped \
|
||||
-p 10.0.1.10:3306:3306 \
|
||||
-e MARIADB_ROOT_PASSWORD='${db_root_password}' \
|
||||
-e MARIADB_DATABASE='${db_name}' \
|
||||
-e MARIADB_USER='${db_user}' \
|
||||
-e MARIADB_PASSWORD='${db_password}' \
|
||||
-v /opt/db/data:/var/lib/mysql \
|
||||
mariadb:11.4
|
||||
159
deploy/terraform/main.tf
Normal file
159
deploy/terraform/main.tf
Normal file
@ -0,0 +1,159 @@
|
||||
locals {
|
||||
db_private_ip = "10.0.1.10"
|
||||
lb_private_ip = "10.0.1.5"
|
||||
app_base_ip = 20 # App-Nodes: 10.0.1.20, .21, ...
|
||||
|
||||
database_url = "mysql://${var.db_user}:${var.db_password}@${local.db_private_ip}:3306/${var.db_name}?serverVersion=11.4.0-MariaDB&charset=utf8mb4"
|
||||
}
|
||||
|
||||
resource "hcloud_ssh_key" "admin" {
|
||||
name = "vcard4-admin"
|
||||
public_key = var.ssh_public_key
|
||||
}
|
||||
|
||||
# --- Privates Netz ---
|
||||
resource "hcloud_network" "net" {
|
||||
name = "vcard4-net"
|
||||
ip_range = "10.0.0.0/16"
|
||||
}
|
||||
|
||||
resource "hcloud_network_subnet" "subnet" {
|
||||
network_id = hcloud_network.net.id
|
||||
type = "cloud"
|
||||
network_zone = var.network_zone
|
||||
ip_range = "10.0.1.0/24"
|
||||
}
|
||||
|
||||
# --- Firewalls ---
|
||||
resource "hcloud_firewall" "app" {
|
||||
name = "vcard4-app-fw"
|
||||
rule {
|
||||
direction = "in"
|
||||
protocol = "tcp"
|
||||
port = "22"
|
||||
source_ips = [var.admin_cidr]
|
||||
}
|
||||
rule {
|
||||
direction = "in"
|
||||
protocol = "tcp"
|
||||
port = "80"
|
||||
source_ips = ["10.0.0.0/16"] # nur aus dem privaten Netz (Load Balancer)
|
||||
}
|
||||
}
|
||||
|
||||
resource "hcloud_firewall" "db" {
|
||||
name = "vcard4-db-fw"
|
||||
rule {
|
||||
direction = "in"
|
||||
protocol = "tcp"
|
||||
port = "22"
|
||||
source_ips = [var.admin_cidr]
|
||||
}
|
||||
rule {
|
||||
direction = "in"
|
||||
protocol = "tcp"
|
||||
port = "3306"
|
||||
source_ips = ["10.0.0.0/16"] # nur App-Nodes
|
||||
}
|
||||
}
|
||||
|
||||
# --- DB-Node ---
|
||||
resource "hcloud_server" "db" {
|
||||
name = "vcard4-db"
|
||||
server_type = var.db_server_type
|
||||
image = "ubuntu-24.04"
|
||||
location = var.location
|
||||
ssh_keys = [hcloud_ssh_key.admin.id]
|
||||
firewall_ids = [hcloud_firewall.db.id]
|
||||
|
||||
user_data = templatefile("${path.module}/cloud-init-db.yaml.tftpl", {
|
||||
db_name = var.db_name
|
||||
db_user = var.db_user
|
||||
db_password = var.db_password
|
||||
db_root_password = var.db_root_password
|
||||
})
|
||||
|
||||
network {
|
||||
network_id = hcloud_network.net.id
|
||||
ip = local.db_private_ip
|
||||
}
|
||||
|
||||
depends_on = [hcloud_network_subnet.subnet]
|
||||
}
|
||||
|
||||
# --- App-Nodes (zustandslos) ---
|
||||
resource "hcloud_server" "app" {
|
||||
count = var.app_count
|
||||
name = "vcard4-app-${count.index + 1}"
|
||||
server_type = var.app_server_type
|
||||
image = "ubuntu-24.04"
|
||||
location = var.location
|
||||
ssh_keys = [hcloud_ssh_key.admin.id]
|
||||
firewall_ids = [hcloud_firewall.app.id]
|
||||
|
||||
user_data = templatefile("${path.module}/cloud-init-app.yaml.tftpl", {
|
||||
repo_url = var.repo_url
|
||||
repo_branch = var.repo_branch
|
||||
run_migrations = count.index == 0 # Migrationen nur auf dem ersten Node
|
||||
app_secret = var.app_secret
|
||||
database_url = local.database_url
|
||||
domain = var.domain
|
||||
cors_allow_origin = "^https?://(www\\.)?${replace(var.domain, ".", "\\.")}$"
|
||||
jwt_passphrase = var.jwt_passphrase
|
||||
jwt_private_key = var.jwt_private_key
|
||||
jwt_public_key = var.jwt_public_key
|
||||
s3_endpoint = var.s3_endpoint
|
||||
s3_region = var.s3_region
|
||||
s3_bucket = var.s3_bucket
|
||||
s3_key = var.s3_key
|
||||
s3_secret = var.s3_secret
|
||||
})
|
||||
|
||||
network {
|
||||
network_id = hcloud_network.net.id
|
||||
ip = "10.0.1.${local.app_base_ip + count.index}"
|
||||
}
|
||||
|
||||
depends_on = [hcloud_network_subnet.subnet, hcloud_server.db]
|
||||
}
|
||||
|
||||
# --- Load Balancer ---
|
||||
resource "hcloud_load_balancer" "lb" {
|
||||
name = "vcard4-lb"
|
||||
load_balancer_type = "lb11"
|
||||
location = var.location
|
||||
}
|
||||
|
||||
resource "hcloud_load_balancer_network" "lb_net" {
|
||||
load_balancer_id = hcloud_load_balancer.lb.id
|
||||
network_id = hcloud_network.net.id
|
||||
ip = local.lb_private_ip
|
||||
}
|
||||
|
||||
resource "hcloud_load_balancer_target" "app" {
|
||||
count = var.app_count
|
||||
type = "server"
|
||||
load_balancer_id = hcloud_load_balancer.lb.id
|
||||
server_id = hcloud_server.app[count.index].id
|
||||
use_private_ip = true
|
||||
depends_on = [hcloud_load_balancer_network.lb_net]
|
||||
}
|
||||
|
||||
resource "hcloud_load_balancer_service" "http" {
|
||||
load_balancer_id = hcloud_load_balancer.lb.id
|
||||
protocol = "http"
|
||||
listen_port = 80
|
||||
destination_port = 80
|
||||
|
||||
health_check {
|
||||
protocol = "http"
|
||||
port = 80
|
||||
interval = 10
|
||||
timeout = 5
|
||||
retries = 3
|
||||
http {
|
||||
path = "/health"
|
||||
status_codes = ["2??"]
|
||||
}
|
||||
}
|
||||
}
|
||||
14
deploy/terraform/outputs.tf
Normal file
14
deploy/terraform/outputs.tf
Normal file
@ -0,0 +1,14 @@
|
||||
output "load_balancer_ip" {
|
||||
description = "Öffentliche IP des Load Balancers → hierauf den DNS-A-Record setzen"
|
||||
value = hcloud_load_balancer.lb.ipv4
|
||||
}
|
||||
|
||||
output "app_server_ips" {
|
||||
description = "Öffentliche IPs der App-Nodes (SSH/Debug)"
|
||||
value = hcloud_server.app[*].ipv4_address
|
||||
}
|
||||
|
||||
output "db_server_ip" {
|
||||
description = "Öffentliche IP des DB-Nodes (SSH)"
|
||||
value = hcloud_server.db.ipv4_address
|
||||
}
|
||||
42
deploy/terraform/terraform.tfvars.example
Normal file
42
deploy/terraform/terraform.tfvars.example
Normal file
@ -0,0 +1,42 @@
|
||||
# Kopiere nach terraform.tfvars und fülle die Werte. NICHT committen (steht in .gitignore).
|
||||
|
||||
hcloud_token = "DEIN_HETZNER_API_TOKEN"
|
||||
ssh_public_key = "ssh-ed25519 AAAA... dein-key"
|
||||
admin_cidr = "1.2.3.4/32" # deine IP für SSH
|
||||
|
||||
location = "nbg1"
|
||||
network_zone = "eu-central"
|
||||
|
||||
app_count = 2
|
||||
app_server_type = "cx22"
|
||||
db_server_type = "cx22"
|
||||
|
||||
# Anwendung
|
||||
repo_url = "https://github.com/DEIN-USER/vcard4reseller.git" # privat: Token in URL
|
||||
repo_branch = "main"
|
||||
domain = "test.example.com"
|
||||
app_secret = "GENERIERE_32_HEX" # z. B. openssl rand -hex 16
|
||||
|
||||
# Datenbank
|
||||
db_password = "STARKES_PASSWORT"
|
||||
db_root_password = "STARKES_ROOT_PASSWORT"
|
||||
|
||||
# JWT (einmal erzeugen, identisch für alle Nodes – siehe README)
|
||||
jwt_passphrase = "DEINE_PASSPHRASE"
|
||||
jwt_private_key = <<-EOT
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
...
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
EOT
|
||||
jwt_public_key = <<-EOT
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
...
|
||||
-----END PUBLIC KEY-----
|
||||
EOT
|
||||
|
||||
# Hetzner Object Storage (Bucket + Keys vorab in der Console anlegen)
|
||||
s3_endpoint = "https://nbg1.your-objectstorage.com"
|
||||
s3_region = "nbg1"
|
||||
s3_bucket = "vcard4-card-assets"
|
||||
s3_key = "OBJECT_STORAGE_ACCESS_KEY"
|
||||
s3_secret = "OBJECT_STORAGE_SECRET_KEY"
|
||||
131
deploy/terraform/variables.tf
Normal file
131
deploy/terraform/variables.tf
Normal file
@ -0,0 +1,131 @@
|
||||
variable "hcloud_token" {
|
||||
description = "Hetzner Cloud API Token (Projekt → Security → API Tokens, Read&Write)"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "location" {
|
||||
description = "Hetzner Standort"
|
||||
type = string
|
||||
default = "nbg1"
|
||||
}
|
||||
|
||||
variable "network_zone" {
|
||||
description = "Netzwerk-Zone passend zum Standort (eu-central für nbg1/fsn1/hel1)"
|
||||
type = string
|
||||
default = "eu-central"
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "Öffentlicher SSH-Schlüssel für Server-Zugang"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "admin_cidr" {
|
||||
description = "CIDR, das per SSH auf die Server darf (z. B. deine IP/32)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_count" {
|
||||
description = "Anzahl App-Nodes (für den Skalierungstest >= 2)"
|
||||
type = number
|
||||
default = 2
|
||||
}
|
||||
|
||||
variable "app_server_type" {
|
||||
description = "Servertyp App-Nodes"
|
||||
type = string
|
||||
default = "cx22"
|
||||
}
|
||||
|
||||
variable "db_server_type" {
|
||||
description = "Servertyp DB-Node"
|
||||
type = string
|
||||
default = "cx22"
|
||||
}
|
||||
|
||||
# --- Anwendung / Deploy ---
|
||||
variable "repo_url" {
|
||||
description = "Git-URL des Repos (per cloud-init geklont; bei privat: Deploy-Token in der URL)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "repo_branch" {
|
||||
description = "Zu deployender Branch"
|
||||
type = string
|
||||
default = "main"
|
||||
}
|
||||
|
||||
variable "domain" {
|
||||
description = "Öffentliche Domain (für CORS, Profil-URLs, später TLS)"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_secret" {
|
||||
description = "Symfony APP_SECRET"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "db_name" {
|
||||
type = string
|
||||
default = "vcard4reseller"
|
||||
}
|
||||
|
||||
variable "db_user" {
|
||||
type = string
|
||||
default = "app"
|
||||
}
|
||||
|
||||
variable "db_password" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "db_root_password" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "jwt_passphrase" {
|
||||
description = "Passphrase der JWT-Schlüssel (identisch zu den erzeugten Keys)"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "jwt_private_key" {
|
||||
description = "Inhalt von config/jwt/private.pem (auf ALLEN Nodes identisch)"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "jwt_public_key" {
|
||||
description = "Inhalt von config/jwt/public.pem"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
# --- Hetzner Object Storage (S3) ---
|
||||
variable "s3_endpoint" {
|
||||
description = "z. B. https://nbg1.your-objectstorage.com"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "s3_region" {
|
||||
type = string
|
||||
default = "nbg1"
|
||||
}
|
||||
|
||||
variable "s3_bucket" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "s3_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "s3_secret" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
13
deploy/terraform/versions.tf
Normal file
13
deploy/terraform/versions.tf
Normal file
@ -0,0 +1,13 @@
|
||||
terraform {
|
||||
required_version = ">= 1.6"
|
||||
required_providers {
|
||||
hcloud = {
|
||||
source = "hetznercloud/hcloud"
|
||||
version = "~> 1.48"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "hcloud" {
|
||||
token = var.hcloud_token
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user