Deployment: Caddy-Edge (TLS + On-Demand für Custom-Domains) + Hetzner DNS
- Caddy ersetzt den Hetzner-LB: terminiert TLS (Portal-Domain automatisch) und load-balanced per reverse_proxy über die App-Nodes. Für Custom-Domains (§11) On-Demand-TLS, autorisiert über GET /internal/tls-allowed. - TlsCheckController + DomainRepository::findVerifiedByHostname: erlaubt Zertifikate nur für Portal-Domain oder verifizierte Domains (Schutz vor Cert-Flooding). - Terraform: hcloud_load_balancer entfernt, Caddy-Server + Firewall (80/443) + cloud-init-caddy (Caddyfile templated mit Upstreams/Domain/ACME). - Optional Hetzner DNS via API (manage_dns): A-Record Portal + Wildcard → Caddy. - nginx.prod: /internal zu Symfony geroutet; APP_PORTAL_DOMAIN-Env. Validiert: Caddyfile (caddy validate), Terraform (validate), /internal/tls-allowed (200/403/400). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c3e05257cb
commit
79e996ab03
@ -46,6 +46,11 @@ JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
|||||||
JWT_PASSPHRASE=d75959918d9ccc5c89c62edbd6e6c6af82d6e2a3d303c53a6f3328e94a05b60a
|
JWT_PASSPHRASE=d75959918d9ccc5c89c62edbd6e6c6af82d6e2a3d303c53a6f3328e94a05b60a
|
||||||
###< lexik/jwt-authentication-bundle ###
|
###< lexik/jwt-authentication-bundle ###
|
||||||
|
|
||||||
|
###> App ###
|
||||||
|
# Portal-Domain (für On-Demand-TLS-Autorisierung). In Prod auf die echte Domain setzen.
|
||||||
|
APP_PORTAL_DOMAIN=localhost
|
||||||
|
###< App ###
|
||||||
|
|
||||||
###> S3 / Object Storage (Druck-Assets) ###
|
###> S3 / Object Storage (Druck-Assets) ###
|
||||||
# Lokal: MinIO. Prod: Hetzner Object Storage (Werte in .env.local / Server-Env setzen).
|
# Lokal: MinIO. Prod: Hetzner Object Storage (Werte in .env.local / Server-Env setzen).
|
||||||
S3_ENDPOINT=http://minio:9000
|
S3_ENDPOINT=http://minio:9000
|
||||||
|
|||||||
44
backend/src/Controller/TlsCheckController.php
Normal file
44
backend/src/Controller/TlsCheckController.php
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\DomainRepository;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On-Demand-TLS-Autorisierung für Caddy (KONZEPT §11): Caddy fragt vor dem
|
||||||
|
* Ausstellen eines Let's-Encrypt-Zertifikats hier nach, ob die Domain erlaubt ist.
|
||||||
|
* Erlaubt = Portal-Domain oder eine verifizierte Custom-Domain aus der DB.
|
||||||
|
* 200 → ausstellen, sonst ablehnen (verhindert unbegrenzte Zertifikatsanfragen).
|
||||||
|
*/
|
||||||
|
final class TlsCheckController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Autowire('%env(APP_PORTAL_DOMAIN)%')]
|
||||||
|
private readonly string $portalDomain,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/internal/tls-allowed', name: 'tls_allowed', methods: ['GET'])]
|
||||||
|
public function __invoke(Request $request, DomainRepository $domains): Response
|
||||||
|
{
|
||||||
|
$host = strtolower(trim((string) $request->query->get('domain')));
|
||||||
|
if ('' === $host) {
|
||||||
|
return new Response('missing domain', 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
$portal = strtolower($this->portalDomain);
|
||||||
|
if ($host === $portal || $host === 'www.'.$portal) {
|
||||||
|
return new Response('ok', 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $domains->findVerifiedByHostname($host)) {
|
||||||
|
return new Response('ok', 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response('not allowed', 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,4 +15,10 @@ class DomainRepository extends ServiceEntityRepository
|
|||||||
{
|
{
|
||||||
parent::__construct($registry, Domain::class);
|
parent::__construct($registry, Domain::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Verifizierte (TLS-fähige) Domain anhand des Hostnamens. */
|
||||||
|
public function findVerifiedByHostname(string $hostname): ?Domain
|
||||||
|
{
|
||||||
|
return $this->findOneBy(['hostname' => $hostname, 'status' => Domain::STATUS_VERIFIED]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,14 +3,22 @@
|
|||||||
Provisioniert einen **Multi-Node-Test** mit nachgewiesener Skalierbarkeit:
|
Provisioniert einen **Multi-Node-Test** mit nachgewiesener Skalierbarkeit:
|
||||||
|
|
||||||
```
|
```
|
||||||
Hetzner Load Balancer (lb11, :80 → /health)
|
Internet (HTTPS)
|
||||||
/ \
|
│
|
||||||
vcard4-app-1 vcard4-app-2 (zustandslose App-Nodes)
|
vcard4-caddy ── TLS: Portal-Domain (automatisch) + Custom-Domains (On-Demand)
|
||||||
\ /
|
(Reverse-Proxy + Load-Balancing)
|
||||||
┌─────────┴───────────┐ ┌─────┴──────────────────┐
|
/ \
|
||||||
vcard4-db (MariaDB, Volume) Hetzner Object Storage (S3, Assets)
|
vcard4-app-1 vcard4-app-2 (zustandslose App-Nodes, HTTP im privaten Netz)
|
||||||
|
\ /
|
||||||
|
┌─────────┴──────────┐ ┌────────┴───────────────┐
|
||||||
|
vcard4-db (MariaDB+Vol) Hetzner Object Storage (S3, Assets)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Caddy** übernimmt TLS (Let's Encrypt) und Load-Balancing — kein Hetzner-LB nötig.
|
||||||
|
Für **Custom-Domains** der Firmenkunden (§11) macht Caddy *On-Demand-TLS* und fragt
|
||||||
|
vorher `GET /internal/tls-allowed?domain=…` in der App (Portal-Domain oder verifizierte
|
||||||
|
Domain aus der DB) → schützt vor unbegrenzten Zertifikatsanfragen.
|
||||||
|
|
||||||
App-Nodes sind zustandslos (Code + Docker), **State** liegt in DB (eigene VM) und
|
App-Nodes sind zustandslos (Code + Docker), **State** liegt in DB (eigene VM) und
|
||||||
Object Storage (Hintergrund-PDFs/Schriften). Dadurch beliebig horizontal skalierbar.
|
Object Storage (Hintergrund-PDFs/Schriften). Dadurch beliebig horizontal skalierbar.
|
||||||
|
|
||||||
@ -27,8 +35,9 @@ Object Storage (Hintergrund-PDFs/Schriften). Dadurch beliebig horizontal skalier
|
|||||||
# → backend/config/jwt/private.pem & public.pem + Passphrase aus .env (JWT_PASSPHRASE)
|
# → backend/config/jwt/private.pem & public.pem + Passphrase aus .env (JWT_PASSPHRASE)
|
||||||
```
|
```
|
||||||
Inhalt der beiden PEM-Dateien + Passphrase in `terraform.tfvars` eintragen.
|
Inhalt der beiden PEM-Dateien + Passphrase in `terraform.tfvars` eintragen.
|
||||||
5. **DNS**: nach `terraform apply` einen A-Record `domain → load_balancer_ip` setzen
|
5. **DNS**: A-Record `domain → caddy_ip` (+ optional `*.zone → caddy_ip` für Subdomains).
|
||||||
(Hetzner DNS ist separat; A-Record auch für die Custom-Domain-Funktion, KONZEPT §11).
|
Entweder manuell **oder** automatisch über die **Hetzner DNS API** (`manage_dns = true`
|
||||||
|
+ `hetzner_dns_token` + `dns_zone_name`; Zone muss bei Hetzner DNS liegen).
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
@ -49,28 +58,36 @@ Deploy-Log auf dem Node: `/var/log/vcard4-deploy.log`.
|
|||||||
## Skalierbarkeit verifizieren
|
## Skalierbarkeit verifizieren
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1) Login + Token holen (LB-IP oder Domain)
|
# 1) Login über die Domain (Caddy → App-Nodes)
|
||||||
TOKEN=$(curl -s -X POST http://<LB-IP>/api/login \
|
TOKEN=$(curl -s -X POST https://<domain>/api/login \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"email":"admin@vcard4reseller.de","password":"admin"}' | jq -r .token)
|
-d '{"email":"admin@vcard4reseller.de","password":"admin"}' | jq -r .token)
|
||||||
|
|
||||||
# 2) Health über den LB (verteilt auf beide Nodes)
|
# 2) Health über Caddy (round-robin auf beide Nodes)
|
||||||
for i in $(seq 1 6); do curl -s http://<LB-IP>/health; echo; done
|
for i in $(seq 1 6); do curl -s https://<domain>/health; echo; done
|
||||||
|
|
||||||
# 3) Cross-Node-Beweis: Hintergrund über den LB hochladen, dann
|
# 3) Cross-Node-Beweis: Hintergrund über die Domain hochladen, dann
|
||||||
# auf BEIDEN Nodes rendern (per SSH) – identische PDFs aus Object Storage:
|
# 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-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'
|
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).
|
## Skalieren
|
||||||
|
|
||||||
|
`app_count` erhöhen → `terraform apply` legt neue App-Nodes an. **Achtung:** die
|
||||||
|
Caddy-Upstreams stehen in der Caddyfile (per cloud-init beim ersten Boot gerendert) —
|
||||||
|
neue Nodes werden **nicht automatisch** aufgenommen. Optionen: Caddy-Node neu erstellen
|
||||||
|
(`terraform taint hcloud_server.caddy && apply`) oder Caddyfile auf dem Caddy-Node
|
||||||
|
aktualisieren + `docker exec caddy caddy reload`. (Später besser: Caddy-Config-Templating
|
||||||
|
per CI oder Service-Discovery.)
|
||||||
|
|
||||||
## Noch offen / Hinweise
|
## Noch offen / Hinweise
|
||||||
|
|
||||||
- **TLS**: aktuell HTTP am LB. Für HTTPS entweder Hetzner **Managed Certificate**
|
- **TLS**: erledigt Caddy (Portal automatisch, Custom-Domains On-Demand). Erste
|
||||||
am LB (DNS bei Hetzner) oder **Caddy** auf den Nodes (On-Demand-TLS) — letzteres
|
Zertifikatsausstellung dauert ein paar Sekunden nach korrektem DNS.
|
||||||
ist auch der Weg für die **Custom-Domains** der Firmenkunden (§11).
|
- **Trusted Proxies**: für korrekte absolute URLs hinter Caddy
|
||||||
- **Trusted Proxies**: für korrekte absolute URLs hinter dem LB
|
|
||||||
`framework.trusted_proxies` auf `%env(TRUSTED_PROXIES)%` setzen.
|
`framework.trusted_proxies` auf `%env(TRUSTED_PROXIES)%` setzen.
|
||||||
|
- **Host-basiertes Routing** (Custom-Domain → richtige Firmenseite) ist §11-Folgearbeit;
|
||||||
|
Caddy stellt bereits Zertifikate für verifizierte Domains aus.
|
||||||
- **Seed**: optional einmalig `app:seed` auf `app-1` für Demo-Daten.
|
- **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).
|
- **Updates**: neuen Stand ausrollen = auf den App-Nodes `git pull` + `docker compose ... up -d --build` (später per CI/Skript).
|
||||||
|
|||||||
@ -11,8 +11,8 @@ server {
|
|||||||
|
|
||||||
client_max_body_size 32m;
|
client_max_body_size 32m;
|
||||||
|
|
||||||
# Symfony-Pfade (API + serverseitige öffentliche Seiten)
|
# Symfony-Pfade (API + serverseitige öffentliche Seiten + interne Endpunkte)
|
||||||
location ~ ^/(api|p|t|css|bundles|health)(/|$) {
|
location ~ ^/(api|p|t|css|bundles|health|internal)(/|$) {
|
||||||
root /app/public;
|
root /app/public;
|
||||||
try_files $uri /index.php$is_args$args;
|
try_files $uri /index.php$is_args$args;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ write_files:
|
|||||||
APP_ENV=prod
|
APP_ENV=prod
|
||||||
APP_DEBUG=0
|
APP_DEBUG=0
|
||||||
APP_SECRET=${app_secret}
|
APP_SECRET=${app_secret}
|
||||||
|
APP_PORTAL_DOMAIN=${domain}
|
||||||
DATABASE_URL="${database_url}"
|
DATABASE_URL="${database_url}"
|
||||||
CORS_ALLOW_ORIGIN=${cors_allow_origin}
|
CORS_ALLOW_ORIGIN=${cors_allow_origin}
|
||||||
TRUSTED_PROXIES=10.0.0.0/16
|
TRUSTED_PROXIES=10.0.0.0/16
|
||||||
|
|||||||
40
deploy/terraform/cloud-init-caddy.yaml.tftpl
Normal file
40
deploy/terraform/cloud-init-caddy.yaml.tftpl
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
#cloud-config
|
||||||
|
package_update: true
|
||||||
|
write_files:
|
||||||
|
- path: /opt/caddy/Caddyfile
|
||||||
|
permissions: '0644'
|
||||||
|
content: |
|
||||||
|
{
|
||||||
|
email ${acme_email}
|
||||||
|
on_demand_tls {
|
||||||
|
# Caddy fragt die App, ob es für die Domain ein Zertifikat ausstellen darf
|
||||||
|
ask http://${ask_upstream}/internal/tls-allowed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Portal (Haupt-Domain): automatisches TLS, Load-Balancing über die App-Nodes
|
||||||
|
${domain}, www.${domain} {
|
||||||
|
reverse_proxy ${app_upstreams} {
|
||||||
|
lb_policy round_robin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Custom-Domains der Firmenkunden: On-Demand-TLS (nur erlaubte Hosts)
|
||||||
|
https:// {
|
||||||
|
tls {
|
||||||
|
on_demand
|
||||||
|
}
|
||||||
|
reverse_proxy ${app_upstreams} {
|
||||||
|
lb_policy round_robin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runcmd:
|
||||||
|
- command -v docker >/dev/null 2>&1 || curl -fsSL https://get.docker.com | sh
|
||||||
|
- mkdir -p /opt/caddy/data /opt/caddy/config
|
||||||
|
- |
|
||||||
|
docker run -d --name caddy --restart unless-stopped \
|
||||||
|
-p 80:80 -p 443:443 -p 443:443/udp \
|
||||||
|
-v /opt/caddy/Caddyfile:/etc/caddy/Caddyfile:ro \
|
||||||
|
-v /opt/caddy/data:/data \
|
||||||
|
-v /opt/caddy/config:/config \
|
||||||
|
caddy:2
|
||||||
32
deploy/terraform/dns.tf
Normal file
32
deploy/terraform/dns.tf
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Optional: DNS-Records über die Hetzner DNS API anlegen (manage_dns = true).
|
||||||
|
# Voraussetzung: Zone liegt bei Hetzner DNS, separater DNS-API-Token.
|
||||||
|
|
||||||
|
data "hetznerdns_zone" "zone" {
|
||||||
|
count = var.manage_dns ? 1 : 0
|
||||||
|
name = var.dns_zone_name
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
# Relativer Record-Name: "@" wenn Portal == Zone, sonst der Subdomain-Teil
|
||||||
|
portal_record_name = var.domain == var.dns_zone_name ? "@" : replace(var.domain, ".${var.dns_zone_name}", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Portal-Domain → Caddy
|
||||||
|
resource "hetznerdns_record" "portal" {
|
||||||
|
count = var.manage_dns ? 1 : 0
|
||||||
|
zone_id = data.hetznerdns_zone.zone[0].id
|
||||||
|
name = local.portal_record_name
|
||||||
|
type = "A"
|
||||||
|
value = hcloud_server.caddy.ipv4_address
|
||||||
|
ttl = 300
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wildcard für Firmen-Subdomains (KONZEPT §11) → Caddy (On-Demand-TLS)
|
||||||
|
resource "hetznerdns_record" "wildcard" {
|
||||||
|
count = var.manage_dns ? 1 : 0
|
||||||
|
zone_id = data.hetznerdns_zone.zone[0].id
|
||||||
|
name = "*"
|
||||||
|
type = "A"
|
||||||
|
value = hcloud_server.caddy.ipv4_address
|
||||||
|
ttl = 300
|
||||||
|
}
|
||||||
@ -1,9 +1,13 @@
|
|||||||
locals {
|
locals {
|
||||||
db_private_ip = "10.0.1.10"
|
db_private_ip = "10.0.1.10"
|
||||||
lb_private_ip = "10.0.1.5"
|
caddy_private_ip = "10.0.1.5"
|
||||||
app_base_ip = 20 # App-Nodes: 10.0.1.20, .21, ...
|
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"
|
database_url = "mysql://${var.db_user}:${var.db_password}@${local.db_private_ip}:3306/${var.db_name}?serverVersion=11.4.0-MariaDB&charset=utf8mb4"
|
||||||
|
|
||||||
|
# Caddy-Upstreams = private IPs der App-Nodes (:80)
|
||||||
|
app_upstreams = join(" ", [for i in range(var.app_count) : "10.0.1.${local.app_base_ip + i}:80"])
|
||||||
|
ask_upstream = "10.0.1.${local.app_base_ip}" # app-1 für die On-Demand-TLS-Abfrage
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "hcloud_ssh_key" "admin" {
|
resource "hcloud_ssh_key" "admin" {
|
||||||
@ -117,43 +121,54 @@ resource "hcloud_server" "app" {
|
|||||||
depends_on = [hcloud_network_subnet.subnet, hcloud_server.db]
|
depends_on = [hcloud_network_subnet.subnet, hcloud_server.db]
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- Load Balancer ---
|
# --- Caddy-Edge (TLS-Terminierung + Reverse-Proxy/Load-Balancing) ---
|
||||||
resource "hcloud_load_balancer" "lb" {
|
resource "hcloud_firewall" "caddy" {
|
||||||
name = "vcard4-lb"
|
name = "vcard4-caddy-fw"
|
||||||
load_balancer_type = "lb11"
|
rule {
|
||||||
location = var.location
|
direction = "in"
|
||||||
}
|
protocol = "tcp"
|
||||||
|
port = "22"
|
||||||
resource "hcloud_load_balancer_network" "lb_net" {
|
source_ips = [var.admin_cidr]
|
||||||
load_balancer_id = hcloud_load_balancer.lb.id
|
}
|
||||||
network_id = hcloud_network.net.id
|
rule {
|
||||||
ip = local.lb_private_ip
|
direction = "in"
|
||||||
}
|
protocol = "tcp"
|
||||||
|
port = "80"
|
||||||
resource "hcloud_load_balancer_target" "app" {
|
source_ips = ["0.0.0.0/0", "::/0"]
|
||||||
count = var.app_count
|
}
|
||||||
type = "server"
|
rule {
|
||||||
load_balancer_id = hcloud_load_balancer.lb.id
|
direction = "in"
|
||||||
server_id = hcloud_server.app[count.index].id
|
protocol = "tcp"
|
||||||
use_private_ip = true
|
port = "443"
|
||||||
depends_on = [hcloud_load_balancer_network.lb_net]
|
source_ips = ["0.0.0.0/0", "::/0"]
|
||||||
}
|
}
|
||||||
|
rule {
|
||||||
resource "hcloud_load_balancer_service" "http" {
|
direction = "in"
|
||||||
load_balancer_id = hcloud_load_balancer.lb.id
|
protocol = "udp"
|
||||||
protocol = "http"
|
port = "443"
|
||||||
listen_port = 80
|
source_ips = ["0.0.0.0/0", "::/0"]
|
||||||
destination_port = 80
|
|
||||||
|
|
||||||
health_check {
|
|
||||||
protocol = "http"
|
|
||||||
port = 80
|
|
||||||
interval = 10
|
|
||||||
timeout = 5
|
|
||||||
retries = 3
|
|
||||||
http {
|
|
||||||
path = "/health"
|
|
||||||
status_codes = ["2??"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resource "hcloud_server" "caddy" {
|
||||||
|
name = "vcard4-caddy"
|
||||||
|
server_type = var.app_server_type
|
||||||
|
image = "ubuntu-24.04"
|
||||||
|
location = var.location
|
||||||
|
ssh_keys = [hcloud_ssh_key.admin.id]
|
||||||
|
firewall_ids = [hcloud_firewall.caddy.id]
|
||||||
|
|
||||||
|
user_data = templatefile("${path.module}/cloud-init-caddy.yaml.tftpl", {
|
||||||
|
acme_email = var.acme_email
|
||||||
|
domain = var.domain
|
||||||
|
app_upstreams = local.app_upstreams
|
||||||
|
ask_upstream = local.ask_upstream
|
||||||
|
})
|
||||||
|
|
||||||
|
network {
|
||||||
|
network_id = hcloud_network.net.id
|
||||||
|
ip = local.caddy_private_ip
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [hcloud_network_subnet.subnet, hcloud_server.app]
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
output "load_balancer_ip" {
|
output "caddy_ip" {
|
||||||
description = "Öffentliche IP des Load Balancers → hierauf den DNS-A-Record setzen"
|
description = "Öffentliche IP des Caddy-Edge → hierauf den DNS-A-Record (Portal + *.) setzen"
|
||||||
value = hcloud_load_balancer.lb.ipv4
|
value = hcloud_server.caddy.ipv4_address
|
||||||
}
|
}
|
||||||
|
|
||||||
output "app_server_ips" {
|
output "app_server_ips" {
|
||||||
|
|||||||
@ -14,8 +14,14 @@ db_server_type = "cx22"
|
|||||||
# Anwendung
|
# Anwendung
|
||||||
repo_url = "https://github.com/DEIN-USER/vcard4reseller.git" # privat: Token in URL
|
repo_url = "https://github.com/DEIN-USER/vcard4reseller.git" # privat: Token in URL
|
||||||
repo_branch = "main"
|
repo_branch = "main"
|
||||||
domain = "test.example.com"
|
domain = "test.example.com" # Portal-Domain (ins Portal einloggen)
|
||||||
app_secret = "GENERIERE_32_HEX" # z. B. openssl rand -hex 16
|
acme_email = "admin@example.com" # Let's Encrypt
|
||||||
|
app_secret = "GENERIERE_32_HEX" # z. B. openssl rand -hex 16
|
||||||
|
|
||||||
|
# DNS optional über Hetzner DNS API (sonst A-Record manuell auf caddy_ip setzen)
|
||||||
|
manage_dns = false
|
||||||
|
hetzner_dns_token = "" # separater DNS-API-Token
|
||||||
|
dns_zone_name = "" # z. B. example.com
|
||||||
|
|
||||||
# Datenbank
|
# Datenbank
|
||||||
db_password = "STARKES_PASSWORT"
|
db_password = "STARKES_PASSWORT"
|
||||||
|
|||||||
@ -57,10 +57,35 @@ variable "repo_branch" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "domain" {
|
variable "domain" {
|
||||||
description = "Öffentliche Domain (für CORS, Profil-URLs, später TLS)"
|
description = "Öffentliche Portal-Domain (CORS, Profil-URLs, TLS)"
|
||||||
type = string
|
type = string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "acme_email" {
|
||||||
|
description = "E-Mail für Let's Encrypt (Caddy ACME)"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- DNS (optional, Hetzner DNS API) ---
|
||||||
|
variable "manage_dns" {
|
||||||
|
description = "true = A-Records (Portal + Wildcard) per Hetzner DNS anlegen"
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "hetzner_dns_token" {
|
||||||
|
description = "Hetzner DNS API Token (separat vom Cloud-Token; nur bei manage_dns)"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "dns_zone_name" {
|
||||||
|
description = "DNS-Zone bei Hetzner DNS (z. B. example.com), nur bei manage_dns"
|
||||||
|
type = string
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
variable "app_secret" {
|
variable "app_secret" {
|
||||||
description = "Symfony APP_SECRET"
|
description = "Symfony APP_SECRET"
|
||||||
type = string
|
type = string
|
||||||
|
|||||||
@ -5,9 +5,17 @@ terraform {
|
|||||||
source = "hetznercloud/hcloud"
|
source = "hetznercloud/hcloud"
|
||||||
version = "~> 1.48"
|
version = "~> 1.48"
|
||||||
}
|
}
|
||||||
|
hetznerdns = {
|
||||||
|
source = "germanbrew/hetznerdns"
|
||||||
|
version = "~> 3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provider "hcloud" {
|
provider "hcloud" {
|
||||||
token = var.hcloud_token
|
token = var.hcloud_token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provider "hetznerdns" {
|
||||||
|
api_token = var.hetzner_dns_token
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user