Terraform dla VPS — provisioning Hetzner Cloud i DigitalOcean krok po kroku
Opublikowano: 10 kwietnia 2026 · Kategoria: VPS
Ręczne tworzenie serwerów przez panel dostawcy działa dla jednego lub dwóch VPS-ów. Przy większej skali — wiele środowisk, wiele regionów, kilkanaście serwerów — ręczne zarządzanie staje się podatne na błędy i niepowtarzalne. Terraform rozwiązuje ten problem: opisujesz infrastrukturę w plikach tekstowych, wrzucasz do gita, a Terraform tworzy i zarządza zasobami przez API dostawcy. Ten artykuł pokazuje pełną konfigurację dla Hetzner Cloud (popularny dostawca VPS) i DigitalOcean, z remote state, modułami i workspace.
Instalacja Terraform
# Ubuntu / Debian wget -O- https://apt.releases.hashicorp.com/gpg | \ sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \ https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \ sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt update && sudo apt install terraform -y # macOS brew tap hashicorp/tap brew install hashicorp/tap/terraform # Weryfikacja terraform version # Autocomplete (opcjonalnie) terraform -install-autocomplete
Struktura projektu Terraform
my-infra/
├── main.tf # Zasoby (servers, networks, DNS)
├── variables.tf # Deklaracje zmiennych
├── outputs.tf # Outputy (IP, IDs)
├── providers.tf # Konfiguracja providerów
├── backend.tf # Remote state backend
├── terraform.tfvars # Wartości zmiennych (w .gitignore!)
└── modules/
├── vps/ # Modul reuzywany dla serwera
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── firewall/ # Modul dla firewall rules Provider Hetzner Cloud — kompletna konfiguracja
# providers.tf
terraform {
required_version = ">= 1.6"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
provider "hcloud" {
token = var.hcloud_token
} # variables.tf
variable "hcloud_token" {
type = string
description = "Hetzner Cloud API token"
sensitive = true # Ukrywa w logach
}
variable "server_name" {
type = string
description = "Nazwa serwera"
default = "web-prod-01"
}
variable "server_type" {
type = string
description = "Typ serwera (CX11, CX21, CX31...)"
default = "cx21" # 2 vCPU, 4 GB RAM
}
variable "location" {
type = string
description = "Datacenter Hetzner"
default = "nbg1" # Nuremberg
# Opcje: nbg1, fsn1, hel1, ash (US), hil (US), sin (Singapur)
}
variable "ssh_key_name" {
type = string
description = "Nazwa klucza SSH w Hetzner (musi byc juz dodany)"
} # main.tf — zasoby Hetzner
# Klucz SSH (jesli chcesz go tez tworzyc przez Terraform)
resource "hcloud_ssh_key" "main" {
name = "my-key"
public_key = file("~/.ssh/id_ed25519.pub")
}
# Siec prywatna (izolacja backendow)
resource "hcloud_network" "private" {
name = "private-network"
ip_range = "10.0.0.0/8"
}
resource "hcloud_network_subnet" "private" {
network_id = hcloud_network.private.id
type = "cloud"
network_zone = "eu-central"
ip_range = "10.0.1.0/24"
}
# Firewall
resource "hcloud_firewall" "web" {
name = "web-firewall"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
}
# Serwer VPS
resource "hcloud_server" "web" {
name = var.server_name
image = "ubuntu-24.04"
server_type = var.server_type
location = var.location
ssh_keys = [hcloud_ssh_key.main.id]
firewall_ids = [hcloud_firewall.web.id]
backups = true # Automatyczny backup Hetzner (20% ceny serwera)
# Podpnij do sieci prywatnej
network {
network_id = hcloud_network.private.id
ip = "10.0.1.10"
}
# Cloud-init — pierwsze uruchomienie
user_data = templatefile("${path.module}/cloud-init.yml", {
hostname = var.server_name
})
labels = {
environment = "production"
role = "web"
managed_by = "terraform"
}
}
# Floating IP (statyczny IP — nie zmienia sie przy rebuild)
resource "hcloud_floating_ip" "web" {
type = "ipv4"
home_location = var.location
}
resource "hcloud_floating_ip_assignment" "web" {
floating_ip_id = hcloud_floating_ip.web.id
server_id = hcloud_server.web.id
} # outputs.tf
output "server_ip" {
description = "Publiczny IP serwera"
value = hcloud_server.web.ipv4_address
}
output "floating_ip" {
description = "Floating IP (nie zmienia sie przy rebuild)"
value = hcloud_floating_ip.web.ip_address
}
output "private_ip" {
description = "IP w sieci prywatnej"
value = "10.0.1.10"
} Provider DigitalOcean — droplet i DNS
# providers.tf — DigitalOcean
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.36"
}
}
}
provider "digitalocean" {
token = var.do_token
} # main.tf — DigitalOcean Droplet z DNS
data "digitalocean_ssh_key" "main" {
name = "my-laptop" # Klucz SSH juz dodany w DO console
}
resource "digitalocean_droplet" "web" {
name = "web-prod"
image = "ubuntu-24-04-x64"
size = "s-2vcpu-4gb" # 2 vCPU, 4 GB RAM
region = "fra1" # Frankfurt
ssh_keys = [data.digitalocean_ssh_key.main.id]
backups = true
tags = ["production", "web", "terraform"]
}
# DNS — podepnij domene do droplet
resource "digitalocean_domain" "main" {
name = "example.com"
ip_address = digitalocean_droplet.web.ipv4_address
}
resource "digitalocean_record" "www" {
domain = digitalocean_domain.main.id
type = "A"
name = "www"
value = digitalocean_droplet.web.ipv4_address
}
resource "digitalocean_record" "mx" {
domain = digitalocean_domain.main.id
type = "MX"
name = "@"
value = "mail.example.com."
priority = 10
} Remote state — współdzielenie w zespole
# backend.tf — S3-compatible remote state (dziala z MinIO, AWS S3, Cloudflare R2)
terraform {
backend "s3" {
bucket = "terraform-state"
key = "prod/web/terraform.tfstate"
region = "us-east-1" # Wymagane przez AWS SDK, moze byc dowolne
endpoint = "https://minio.example.com" # MinIO endpoint
access_key = "minio-access-key"
secret_key = "minio-secret-key"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
force_path_style = true
# State locking przez DynamoDB (AWS) lub nie uzywaj dla MinIO
# dynamodb_table = "terraform-state-lock"
}
} Workspace — środowiska dev/staging/prod
# Workspace pozwala uzywac tych samych plikow .tf dla roznych srodowisk
terraform workspace list # Lista workspaces (domyslnie: default)
terraform workspace new dev # Utwórz workspace "dev"
terraform workspace new prod
terraform workspace select prod # Przejdz do prod
# Uzywaj workspace.name w zasobach
resource "hcloud_server" "web" {
name = "web-${terraform.workspace}" # web-prod, web-dev
server_type = terraform.workspace == "prod" ? "cx31" : "cx11"
location = terraform.workspace == "prod" ? "nbg1" : "hel1"
}
# Lub uzywaj osobnych .tfvars per workspace
# terraform apply -var-file="envs/${terraform.workspace}.tfvars" Workflow — plan, apply i destroy
# Inicjalizacja projektu (pobiera providery) terraform init # Format i walidacja terraform fmt -recursive # Formatuje wszystkie pliki .tf terraform validate # Sprawdza skladnie bez api calls # Plan — pokazuje co zostanie zmienione (BEZ WYKONANIA) terraform plan terraform plan -var-file="terraform.tfvars" terraform plan -out=tfplan # Zapisz plan do pliku # Apply — wykonaj plan terraform apply terraform apply tfplan # Wykonaj zapisany plan (bez pytania) terraform apply -auto-approve # Bez potwierdzenia (uzyj w CI/CD) # Sprawdz aktualny state terraform show terraform state list # Lista zasobow terraform state show hcloud_server.web # Szczegóły zasobu # Importuj istniejacy zasob do state (gdy serwer jest juz utworzony recznie) terraform import hcloud_server.web 12345678 # Destroy — usun WSZYSTKO (UWAGA: nie ma cofniecia) terraform destroy terraform destroy -target=hcloud_server.web # Usun tylko jeden zasob
Moduły — reużywalna infrastruktura
# modules/vps/main.tf — modul reuzywany
resource "hcloud_server" "this" {
name = var.name
image = var.image
server_type = var.server_type
location = var.location
ssh_keys = var.ssh_key_ids
backups = var.enable_backups
}
# modules/vps/variables.tf
variable "name" { type = string }
variable "image" { type = string; default = "ubuntu-24.04" }
variable "server_type" { type = string; default = "cx21" }
variable "location" { type = string; default = "nbg1" }
variable "ssh_key_ids" { type = list(string) }
variable "enable_backups" { type = bool; default = false }
# modules/vps/outputs.tf
output "id" { value = hcloud_server.this.id }
output "ipv4_address" { value = hcloud_server.this.ipv4_address } # main.tf — uzycie modulu
module "web_server" {
source = "./modules/vps"
name = "web-prod"
server_type = "cx31"
location = "nbg1"
ssh_key_ids = [hcloud_ssh_key.main.id]
enable_backups = true
}
module "db_server" {
source = "./modules/vps"
name = "db-prod"
server_type = "cx41"
location = "nbg1"
ssh_key_ids = [hcloud_ssh_key.main.id]
}
# Publiczne registry modulow (np. terraform.io/modules)
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.0.0"
# ...
} GitOps — CI/CD z Terraform
# .github/workflows/terraform.yml
name: Terraform CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
env:
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.7.x"
- name: Terraform Init
run: terraform init
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
run: terraform plan -out=tfplan
# Apply tylko na push do main (nie na PR)
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan