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:
parent
81c38e8d39
commit
2d515f7206
@ -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.
|
||||
|
||||
171
terraform/hetzner/test/firewall.tf
Normal file
171
terraform/hetzner/test/firewall.tf
Normal 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"
|
||||
}
|
||||
}
|
||||
10
terraform/hetzner/test/locals.tf
Normal file
10
terraform/hetzner/test/locals.tf
Normal 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"
|
||||
}
|
||||
22
terraform/hetzner/test/network.tf
Normal file
22
terraform/hetzner/test/network.tf
Normal 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
|
||||
}
|
||||
46
terraform/hetzner/test/outputs.tf
Normal file
46
terraform/hetzner/test/outputs.tf
Normal 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
|
||||
}
|
||||
}
|
||||
8
terraform/hetzner/test/placement.tf
Normal file
8
terraform/hetzner/test/placement.tf
Normal file
@ -0,0 +1,8 @@
|
||||
resource "hcloud_placement_group" "test_spread" {
|
||||
name = "${local.name_prefix}-spread"
|
||||
type = "spread"
|
||||
|
||||
labels = {
|
||||
environment = var.environment
|
||||
}
|
||||
}
|
||||
3
terraform/hetzner/test/providers.tf
Normal file
3
terraform/hetzner/test/providers.tf
Normal file
@ -0,0 +1,3 @@
|
||||
provider "hcloud" {
|
||||
token = var.hcloud_token
|
||||
}
|
||||
71
terraform/hetzner/test/servers.tf
Normal file
71
terraform/hetzner/test/servers.tf
Normal 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]
|
||||
}
|
||||
8
terraform/hetzner/test/terraform.tfvars.example
Normal file
8
terraform/hetzner/test/terraform.tfvars.example
Normal 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"]
|
||||
46
terraform/hetzner/test/variables.tf
Normal file
46
terraform/hetzner/test/variables.tf
Normal 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"
|
||||
}
|
||||
10
terraform/hetzner/test/versions.tf
Normal file
10
terraform/hetzner/test/versions.tf
Normal file
@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_version = ">= 1.6"
|
||||
|
||||
required_providers {
|
||||
hcloud = {
|
||||
source = "hetznercloud/hcloud"
|
||||
version = "~> 1.49"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user