Terraform Modules i Remote State — organizacja dużej infrastruktury
Opublikowano: 10 kwietnia 2026 · Kategoria: VPS / DevOps
Gdy infrastruktura rośnie powyżej kilku serwerów, płaski plik main.tf staje się niemożliwy
do utrzymania. Terraform modules rozwiązują ten problem analogicznie jak funkcje w programowaniu
— enkapsulują logikę, przyjmują inputs i zwracają outputs. Remote state eliminuje konflikty przy
pracy zespołowej. Ten artykuł pokazuje jak budować modularną infrastrukturę Terraform od prostych
modułów VPS po zaawansowane setupy z Terragrunt.
Tworzenie modułu — struktura i konwencje
Moduł to po prostu katalog z plikami .tf. Konwencja nakazuje trzy główne pliki:
main.tf (zasoby), variables.tf (inputs) i
outputs.tf (exports). Oto przykładowy moduł tworzący VPS na Hetzner Cloud:
# modules/hetzner-vps/variables.tf
variable "server_name" {
description = "Nazwa serwera"
type = string
}
variable "server_type" {
description = "Typ serwera (cx11, cx21, cx31...)"
type = string
default = "cx21"
}
variable "location" {
description = "Lokalizacja datacenter"
type = string
default = "nbg1"
validation {
condition = contains(["nbg1", "fsn1", "hel1"], var.location)
error_message = "Nieprawidłowa lokalizacja. Dozwolone: nbg1, fsn1, hel1."
}
}
variable "ssh_keys" {
description = "Lista nazw kluczy SSH z Hetzner Cloud"
type = list(string)
}
variable "tags" {
description = "Mapa tagów dla serwera"
type = map(string)
default = {}
}
# modules/hetzner-vps/main.tf
locals {
# Locals — obliczone wartości wewnętrzne modułu
all_tags = merge(
{
"managed-by" = "terraform"
"module" = "hetzner-vps"
},
var.tags
)
}
resource "hcloud_server" "main" {
name = var.server_name
server_type = var.server_type
image = "ubuntu-22.04"
location = var.location
ssh_keys = var.ssh_keys
labels = local.all_tags
lifecycle {
ignore_changes = [ssh_keys]
}
}
resource "hcloud_firewall" "main" {
name = "${var.server_name}-firewall"
rule {
direction = "in"
port = "22"
protocol = "tcp"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
port = "80"
protocol = "tcp"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
port = "443"
protocol = "tcp"
source_ips = ["0.0.0.0/0", "::/0"]
}
}
resource "hcloud_firewall_attachment" "main" {
firewall_id = hcloud_firewall.main.id
server_ids = [hcloud_server.main.id]
}
# modules/hetzner-vps/outputs.tf
output "server_id" {
description = "ID serwera"
value = hcloud_server.main.id
}
output "server_ip" {
description = "Publiczny adres IPv4"
value = hcloud_server.main.ipv4_address
}
output "server_ipv6" {
description = "Publiczny adres IPv6"
value = hcloud_server.main.ipv6_address
} Wywoływanie modułu z konfiguracji głównej
# main.tf — wywołanie modułu
module "web_server" {
source = "./modules/hetzner-vps"
server_name = "web-prod-01"
server_type = "cx21"
location = "nbg1"
ssh_keys = ["moj-klucz", "klucz-ci"]
tags = {
"environment" = "production"
"role" = "web"
}
}
module "db_server" {
source = "./modules/hetzner-vps"
server_name = "db-prod-01"
server_type = "cx31" # Większy dla bazy danych
location = "fsn1"
ssh_keys = ["moj-klucz"]
tags = {
"environment" = "production"
"role" = "database"
}
}
# Używanie outputs modułu w innych zasobach
output "web_server_ip" {
value = module.web_server.server_ip
}
# Ansible inventory generowany przez Terraform
resource "local_file" "ansible_inventory" {
content = templatefile("${path.module}/templates/inventory.tmpl", {
web_ip = module.web_server.server_ip
db_ip = module.db_server.server_ip
})
filename = "${path.module}/../ansible/inventory/production"
} Remote State — S3 + DynamoDB locking
Remote state w S3 z lockowaniem przez DynamoDB to najpopularniejsza konfiguracja dla AWS. Dla innych cloudów używasz analogicznych rozwiązań (GCS, Azure Blob, MinIO dla on-premise). Inicjalizacja backendu musi być wykonana raz:
# backend.tf — konfiguracja remote state
terraform {
backend "s3" {
bucket = "moja-firma-terraform-state"
key = "prod/web/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
# Opcjonalne: niestandardowe endpointy (MinIO, DigitalOcean Spaces)
# endpoint = "https://fra1.digitaloceanspaces.com"
# skip_region_validation = true
# force_path_style = true
}
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
# Tworzenie zasobów S3 i DynamoDB (jednorazowo, własną konfiguracją)
# aws s3 mb s3://moja-firma-terraform-state --region eu-central-1
# aws s3api put-bucket-versioning \
# --bucket moja-firma-terraform-state \
# --versioning-configuration Status=Enabled
# aws dynamodb create-table \
# --table-name terraform-state-lock \
# --attribute-definitions AttributeName=LockID,AttributeType=S \
# --key-schema AttributeName=LockID,KeyType=HASH \
# --billing-mode PAY_PER_REQUEST \
# --region eu-central-1
# Inicjalizacja backendu
# terraform init Workspaces — izolacja środowisk
# Zarządzanie workspaces
terraform workspace list # Wylistuj workspaces
terraform workspace new staging # Utwórz workspace
terraform workspace select prod # Przełącz na workspace
terraform workspace show # Pokaż aktualny workspace
terraform workspace delete staging # Usuń (wymaga pustego state)
# Użycie workspace w konfiguracji
locals {
env_config = {
prod = {
server_type = "cx31"
count = 3
}
staging = {
server_type = "cx11"
count = 1
}
dev = {
server_type = "cx11"
count = 1
}
}
current = local.env_config[terraform.workspace]
}
module "web_servers" {
count = local.current.count
source = "./modules/hetzner-vps"
server_name = "${terraform.workspace}-web-${count.index + 1}"
server_type = local.current.server_type
ssh_keys = var.ssh_keys
} Terragrunt — DRY konfiguracja dla wielu modułów
Terragrunt eliminuje powtarzanie bloku backend w każdym module. Struktura katalogów odzwierciedla środowiska i serwisy:
# Struktura projektu z Terragrunt
# infra/
# ├── terragrunt.hcl # Root — wspólna konfiguracja
# ├── prod/
# │ ├── env.hcl # Zmienne środowiska
# │ ├── web-servers/
# │ │ └── terragrunt.hcl # Konfiguracja serwisów
# │ └── database/
# │ └── terragrunt.hcl
# └── staging/
# └── web-servers/
# └── terragrunt.hcl
# Root terragrunt.hcl
locals {
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
env = local.env_vars.locals.environment
}
generate "backend" {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
contents = <<-EOF
terraform {
backend "s3" {
bucket = "moja-firma-tf-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}
EOF
}
# prod/web-servers/terragrunt.hcl
terraform {
source = "../../../modules/hetzner-vps"
}
include "root" {
path = find_in_parent_folders()
}
inputs = {
server_name = "web-prod-01"
server_type = "cx31"
ssh_keys = ["prod-key"]
}
# Uruchomienie wszystkich modułów naraz
# cd infra/prod
# terragrunt run-all plan
# terragrunt run-all apply Porównanie strategii organizacji infrastruktury
| Strategia | Skala | Zalety | Wady |
|---|---|---|---|
| Pojedynczy main.tf | 1-5 zasobów | Prosta, szybka do startu | Nie skaluje się |
| Moduły lokalne | 5-50 zasobów | Reużywalność, DRY | Bez wersjonowania modułów |
| Rejestr publiczny | Dowolna | Gotowe rozwiązania, przetestowane | Zależność zewnętrzna |
| Workspaces | 2-3 środowiska | Prosta izolacja środowisk | Ta sama konfiguracja w każdym ws |
| Terragrunt | 50+ zasobów, wiele środowisk | DRY backend, run-all, dependencies | Dodatkowe narzędzie do nauki |