From 2d515f720692a147ba0bf55fbfd8f21799844078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Murat=20=C3=96ZDEM=C4=B0R?= Date: Sun, 10 May 2026 14:09:23 +0300 Subject: [PATCH] Add initial Terraform infrastructure for Hetzner test environment This commit introduces the foundational Infrastructure-as-Code for provisioning a test environment on Hetzner Cloud. It defines server nodes, private networking, comprehensive firewalls, and includes documentation on resource lifecycle and safe configuration practices. --- setup/01-test-terraform-iaac.md | 40 ++++ terraform/hetzner/test/firewall.tf | 171 ++++++++++++++++++ terraform/hetzner/test/locals.tf | 10 + terraform/hetzner/test/network.tf | 22 +++ terraform/hetzner/test/outputs.tf | 46 +++++ terraform/hetzner/test/placement.tf | 8 + terraform/hetzner/test/providers.tf | 3 + terraform/hetzner/test/servers.tf | 71 ++++++++ .../hetzner/test/terraform.tfvars.example | 8 + terraform/hetzner/test/variables.tf | 46 +++++ terraform/hetzner/test/versions.tf | 10 + 11 files changed, 435 insertions(+) create mode 100644 terraform/hetzner/test/firewall.tf create mode 100644 terraform/hetzner/test/locals.tf create mode 100644 terraform/hetzner/test/network.tf create mode 100644 terraform/hetzner/test/outputs.tf create mode 100644 terraform/hetzner/test/placement.tf create mode 100644 terraform/hetzner/test/providers.tf create mode 100644 terraform/hetzner/test/servers.tf create mode 100644 terraform/hetzner/test/terraform.tfvars.example create mode 100644 terraform/hetzner/test/variables.tf create mode 100644 terraform/hetzner/test/versions.tf diff --git a/setup/01-test-terraform-iaac.md b/setup/01-test-terraform-iaac.md index d9a101f..4b271db 100644 --- a/setup/01-test-terraform-iaac.md +++ b/setup/01-test-terraform-iaac.md @@ -108,6 +108,46 @@ output "test_public_ips" { Inventory output'u daha sonra `ansible/inventory/generated/test.yml` dosyasina yazilabilir. Inventory dosyasinda secret bulunmayacaksa commit edilebilir; secret veya token icerirse commit edilmeyecek. +## Lifecycle ve Resize Politikasi + +### server_type Degisikligi (Yeniden Boyutlandirma) + +`server_type` degistirmek Terraform destroy+create **tetiklemez**. `hcloud` provider +bunu natively destekler: sunucuyu durdurur, Hetzner Resize API'sini cagirir, +yeniden baslatir. `terraform.tfvars` icinde degeri guncelle, `terraform apply` calistir. + +Downtime olur (sunucu durur ve baslar) ancak disk, kurulu yazilim ve Docker volumes +korunur. `ignore_changes` veya manuel adim gerekmez. + +### Hangi Degisiklikler Sunucuyu Zorla Yeniden Olusturur? + +| Degisen alan | Davranis | Not | +| --- | --- | --- | +| `server_type` | In-place resize (provider native) | `terraform apply` yeterli | +| `hcloud_server_network` | Sadece attachment guncellenir | Ayri resource kullanildigi icin | +| `hcloud_firewall_attachment` | Sadece attachment guncellenir | Ayri resource kullanildigi icin | +| `placement_group_id` | Hetzner API degisime izin vermiyor → destroy+create | Degistirme | +| `image` | Disk imaji degisir → destroy+create | Degistirme | +| `location` | Baska datacenter'a tasinamaz → destroy+create | Degistirme | + +### Network ve Firewall Attachment Ayrimi + +`network` blogu ve `firewall_ids` `hcloud_server` icine gomulmez. Bunun yerine +ayri resource tanimlanir: + +- `hcloud_server_network` — private IP atamasi +- `hcloud_firewall_attachment` — firewall iliskisi + +Gomulu tanimlamada bazi provider versiyonlari bu alanlardaki degisiklikleri +sunucu recreation olarak yorumlar. Ayri resource kullanildiginda sadece +attachment guncellenir, sunucu dokunulmaz. + +### prevent_destroy Korumasi + +Her sunucuya `lifecycle { prevent_destroy = true }` eklenir. Bu blok varken +Terraform hicbir kosulda sunucuyu silemez, plan asamasinda hata verir. +Kasitli silmek icin once lifecycle blogunu gecici olarak kaldir. + ## Kabul Kriterleri - `terraform plan` sadece test Hetzner Project token'i ile calisir. diff --git a/terraform/hetzner/test/firewall.tf b/terraform/hetzner/test/firewall.tf new file mode 100644 index 0000000..36d9371 --- /dev/null +++ b/terraform/hetzner/test/firewall.tf @@ -0,0 +1,171 @@ +# Swarm node firewall — public HTTP/HTTPS + private infra services +resource "hcloud_firewall" "swarm" { + name = "${local.name_prefix}-firewall-swarm" + + # SSH — admin CIDRs only + rule { + direction = "in" + protocol = "tcp" + port = "22" + source_ips = var.admin_allowed_cidrs + } + + # HTTP public + rule { + direction = "in" + protocol = "tcp" + port = "80" + source_ips = ["0.0.0.0/0", "::/0"] + } + + # HTTPS public + rule { + direction = "in" + protocol = "tcp" + port = "443" + source_ips = ["0.0.0.0/0", "::/0"] + } + + # Docker Swarm control plane + rule { + direction = "in" + protocol = "tcp" + port = "2377" + source_ips = [local.app_subnet_cidr] + } + + # Docker Swarm node discovery (TCP) + rule { + direction = "in" + protocol = "tcp" + port = "7946" + source_ips = [local.app_subnet_cidr] + } + + # Docker Swarm node discovery (UDP) + rule { + direction = "in" + protocol = "udp" + port = "7946" + source_ips = [local.app_subnet_cidr] + } + + # Docker Swarm VXLAN overlay + rule { + direction = "in" + protocol = "udp" + port = "4789" + source_ips = [local.app_subnet_cidr] + } + + # Vault API — private only, never public + rule { + direction = "in" + protocol = "tcp" + port = "8200" + source_ips = [local.app_subnet_cidr] + } + + # Redis + rule { + direction = "in" + protocol = "tcp" + port = "6379" + source_ips = [local.app_subnet_cidr] + } + + # RabbitMQ AMQP + rule { + direction = "in" + protocol = "tcp" + port = "5672" + source_ips = [local.app_subnet_cidr] + } + + # RabbitMQ STOMP + rule { + direction = "in" + protocol = "tcp" + port = "61613" + source_ips = [local.app_subnet_cidr] + } + + # RabbitMQ Web STOMP + rule { + direction = "in" + protocol = "tcp" + port = "15674" + source_ips = [local.app_subnet_cidr] + } + + # RabbitMQ Management — admin CIDRs only + rule { + direction = "in" + protocol = "tcp" + port = "15672" + source_ips = var.admin_allowed_cidrs + } + + # APISIX Admin API — admin CIDRs only + rule { + direction = "in" + protocol = "tcp" + port = "9180" + source_ips = var.admin_allowed_cidrs + } + + # Prometheus — admin CIDRs only + rule { + direction = "in" + protocol = "tcp" + port = "9090" + source_ips = var.admin_allowed_cidrs + } + + # Grafana — admin CIDRs only + rule { + direction = "in" + protocol = "tcp" + port = "3000" + source_ips = var.admin_allowed_cidrs + } + + labels = { + environment = var.environment + role = "swarm" + } +} + +# DB node firewall — SSH + DB ports from app/swarm subnet only +resource "hcloud_firewall" "db" { + name = "${local.name_prefix}-firewall-db" + + # SSH — admin CIDRs only + rule { + direction = "in" + protocol = "tcp" + port = "22" + source_ips = var.admin_allowed_cidrs + } + + # PostgreSQL from app/swarm subnet + rule { + direction = "in" + protocol = "tcp" + port = "5432" + source_ips = [local.app_subnet_cidr] + } + + # MongoDB from app/swarm subnet + rule { + direction = "in" + protocol = "tcp" + port = "27017" + source_ips = [local.app_subnet_cidr] + } + + labels = { + environment = var.environment + role = "db" + } +} diff --git a/terraform/hetzner/test/locals.tf b/terraform/hetzner/test/locals.tf new file mode 100644 index 0000000..eae13c6 --- /dev/null +++ b/terraform/hetzner/test/locals.tf @@ -0,0 +1,10 @@ +locals { + name_prefix = "iklim-${var.environment}" + + swarm_private_ip = "10.10.10.11" + db_private_ip = "10.10.20.11" + + network_cidr = "10.10.0.0/16" + app_subnet_cidr = "10.10.10.0/24" + db_subnet_cidr = "10.10.20.0/24" +} diff --git a/terraform/hetzner/test/network.tf b/terraform/hetzner/test/network.tf new file mode 100644 index 0000000..357d2ec --- /dev/null +++ b/terraform/hetzner/test/network.tf @@ -0,0 +1,22 @@ +resource "hcloud_network" "main" { + name = "${local.name_prefix}-net" + ip_range = local.network_cidr + + labels = { + environment = var.environment + } +} + +resource "hcloud_network_subnet" "app" { + network_id = hcloud_network.main.id + type = "cloud" + network_zone = "eu-central" + ip_range = local.app_subnet_cidr +} + +resource "hcloud_network_subnet" "db" { + network_id = hcloud_network.main.id + type = "cloud" + network_zone = "eu-central" + ip_range = local.db_subnet_cidr +} diff --git a/terraform/hetzner/test/outputs.tf b/terraform/hetzner/test/outputs.tf new file mode 100644 index 0000000..82092b6 --- /dev/null +++ b/terraform/hetzner/test/outputs.tf @@ -0,0 +1,46 @@ +output "ansible_inventory_yaml" { + description = "Ansible inventory in YAML format — write to ansible/inventory/generated/test.yml" + sensitive = false + value = yamlencode({ + all = { + children = { + swarm = { + hosts = { + (hcloud_server.swarm.name) = { + ansible_host = hcloud_server.swarm.ipv4_address + private_ip = local.swarm_private_ip + ansible_user = "root" + } + } + } + db = { + hosts = { + (hcloud_server.db.name) = { + ansible_host = hcloud_server.db.ipv4_address + private_ip = local.db_private_ip + ansible_user = "root" + } + } + } + } + } + }) +} + +output "test_private_ips" { + description = "Private IPs assigned to test nodes" + sensitive = false + value = { + swarm_01 = local.swarm_private_ip + db_01 = local.db_private_ip + } +} + +output "test_public_ips" { + description = "Public IPv4 addresses of test nodes" + sensitive = false + value = { + swarm_01 = hcloud_server.swarm.ipv4_address + db_01 = hcloud_server.db.ipv4_address + } +} diff --git a/terraform/hetzner/test/placement.tf b/terraform/hetzner/test/placement.tf new file mode 100644 index 0000000..b1b41ed --- /dev/null +++ b/terraform/hetzner/test/placement.tf @@ -0,0 +1,8 @@ +resource "hcloud_placement_group" "test_spread" { + name = "${local.name_prefix}-spread" + type = "spread" + + labels = { + environment = var.environment + } +} diff --git a/terraform/hetzner/test/providers.tf b/terraform/hetzner/test/providers.tf new file mode 100644 index 0000000..706ed68 --- /dev/null +++ b/terraform/hetzner/test/providers.tf @@ -0,0 +1,3 @@ +provider "hcloud" { + token = var.hcloud_token +} diff --git a/terraform/hetzner/test/servers.tf b/terraform/hetzner/test/servers.tf new file mode 100644 index 0000000..143cb2b --- /dev/null +++ b/terraform/hetzner/test/servers.tf @@ -0,0 +1,71 @@ +resource "hcloud_ssh_key" "admin" { + name = "${local.name_prefix}-admin-key" + public_key = file(var.admin_ssh_public_key_path) +} + +resource "hcloud_server" "swarm" { + name = "${var.environment}-swarm-01" + server_type = var.server_type_swarm + image = var.image + location = var.location + ssh_keys = [hcloud_ssh_key.admin.id] + placement_group_id = hcloud_placement_group.test_spread.id + + labels = { + environment = var.environment + role = "swarm" + type = "service" + } + + # prevent_destroy: Terraform'un sunucuyu kazara silmesini engeller. + # Kasitli silmek icin once bu bloku kaldir. + lifecycle { + prevent_destroy = true + } +} + +resource "hcloud_server" "db" { + name = "${var.environment}-db-01" + server_type = var.server_type_db + image = var.image + location = var.location + ssh_keys = [hcloud_ssh_key.admin.id] + placement_group_id = hcloud_placement_group.test_spread.id + + labels = { + environment = var.environment + role = "db" + type = "db" + } + + lifecycle { + prevent_destroy = true + } +} + +# Ayri resource: firewall veya network degistiginde sunucu recreation tetiklenmez. +resource "hcloud_server_network" "swarm" { + server_id = hcloud_server.swarm.id + network_id = hcloud_network.main.id + ip = local.swarm_private_ip + + depends_on = [hcloud_network_subnet.app] +} + +resource "hcloud_server_network" "db" { + server_id = hcloud_server.db.id + network_id = hcloud_network.main.id + ip = local.db_private_ip + + depends_on = [hcloud_network_subnet.db] +} + +resource "hcloud_firewall_attachment" "swarm" { + firewall_id = hcloud_firewall.swarm.id + server_ids = [hcloud_server.swarm.id] +} + +resource "hcloud_firewall_attachment" "db" { + firewall_id = hcloud_firewall.db.id + server_ids = [hcloud_server.db.id] +} diff --git a/terraform/hetzner/test/terraform.tfvars.example b/terraform/hetzner/test/terraform.tfvars.example new file mode 100644 index 0000000..c221ee8 --- /dev/null +++ b/terraform/hetzner/test/terraform.tfvars.example @@ -0,0 +1,8 @@ +hcloud_token = "YOUR_HETZNER_TEST_PROJECT_API_TOKEN" +environment = "test" +location = "fsn1" +image = "ubuntu-24.04" +server_type_swarm = "cx32" +server_type_db = "cx42" +admin_ssh_public_key_path = "~/.ssh/id_ed25519.pub" +admin_allowed_cidrs = ["X.X.X.X/32"] diff --git a/terraform/hetzner/test/variables.tf b/terraform/hetzner/test/variables.tf new file mode 100644 index 0000000..87d06f6 --- /dev/null +++ b/terraform/hetzner/test/variables.tf @@ -0,0 +1,46 @@ +variable "hcloud_token" { + type = string + sensitive = true + description = "Hetzner Cloud API token for the test project" +} + +variable "environment" { + type = string + default = "test" + description = "Environment name prefix for all resources" +} + +variable "location" { + type = string + default = "fsn1" + description = "Hetzner Cloud datacenter location" +} + +variable "image" { + type = string + default = "ubuntu-24.04" + description = "Server image" +} + +variable "server_type_swarm" { + type = string + default = "cx32" + description = "Hetzner server type for the Swarm node" +} + +variable "server_type_db" { + type = string + default = "cx42" + description = "Hetzner server type for the DB node" +} + +variable "admin_ssh_public_key_path" { + type = string + default = "~/.ssh/id_ed25519.pub" + description = "Path to the admin SSH public key file" +} + +variable "admin_allowed_cidrs" { + type = list(string) + description = "CIDR list for admin SSH and management port access" +} diff --git a/terraform/hetzner/test/versions.tf b/terraform/hetzner/test/versions.tf new file mode 100644 index 0000000..d636b4b --- /dev/null +++ b/terraform/hetzner/test/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.6" + + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "~> 1.49" + } + } +}