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.
This commit is contained in:
Murat ÖZDEMİR 2026-05-10 14:09:23 +03:00
parent 81c38e8d39
commit 2d515f7206
11 changed files with 435 additions and 0 deletions

View File

@ -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. 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 ## Kabul Kriterleri
- `terraform plan` sadece test Hetzner Project token'i ile calisir. - `terraform plan` sadece test Hetzner Project token'i ile calisir.

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -0,0 +1,8 @@
resource "hcloud_placement_group" "test_spread" {
name = "${local.name_prefix}-spread"
type = "spread"
labels = {
environment = var.environment
}
}

View File

@ -0,0 +1,3 @@
provider "hcloud" {
token = var.hcloud_token
}

View File

@ -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]
}

View File

@ -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"]

View File

@ -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"
}

View File

@ -0,0 +1,10 @@
terraform {
required_version = ">= 1.6"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.49"
}
}
}