Menu
Szybki wybór
Hosting Domeny VPS SSL Kalkulator Porównania FAQ
Aktywne kody
Wszystkie kody rabatowe

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

Najczęstsze pytania

Czym jest Terraform i do czego go używa się przy VPS? +
Terraform to narzędzie IaC (Infrastructure as Code) — opisujesz infrastrukturę w plikach .tf (HCL), a Terraform tworzy ją przez API providera. Dla VPS: automatyczne tworzenie serwerów, sieci, firewalli, DNS, backupów przez API Hetzner, DigitalOcean, OVH lub innych. Zalety: infrastruktura w git, powtarzalność (identyczne środowiska dev/staging/prod), dokumentacja przez kod, rollback przez git revert, planowanie zmian przed zastosowaniem.
Czym Terraform różni się od Ansible? +
Terraform zarządza ZASOBAMI — tworzy, modyfikuje, usuwa serwery, sieci, DNS (provisioning). Ansible zarządza KONFIGURACJĄ — instaluje pakiety, kopiuje pliki, konfiguruje usługi na istniejącym serwerze. W praktyce używa się obu: Terraform tworzy VPS, Ansible konfiguruje system i aplikacje. Terraform jest deklaratywny (opisujesz stan docelowy), Ansible jest proceduralny (opisujesz kroki).
Co to jest Terraform state i gdzie go przechowywać? +
State (terraform.tfstate) to plik JSON mapujący zasoby Terraform na rzeczywiste zasoby u providera. Terraform porównuje state z plikami .tf żeby wiedzieć co dodać, zmienić lub usunąć. Lokalny state (domyślny) działa dla single-user. W team lub CI/CD używaj remote state: S3 + DynamoDB (AWS), Terraform Cloud, MinIO (self-hosted S3). Remote state zapobiega konfliktom przez state locking i jest współdzielony między członkami zespołu.
Jak bezpiecznie przechowywać tokeny API w Terraform? +
NIGDY nie wpisuj tokenów API bezpośrednio w plikach .tf — trafią do gita. Opcje: (1) Zmienne środowiskowe: export HCLOUD_TOKEN=xxx, Terraform automatycznie je czyta. (2) Plik .tfvars poza gitem (.gitignore): hcloud_token = "xxx". (3) Vault dynamic credentials. (4) GitHub Actions/GitLab CI secrets jako env vars. Dodaj *.tfvars, .terraform/, terraform.tfstate do .gitignore.

Sprawdź oferty pasujące do tego scenariusza

Poniżej masz szybkie przejścia do ofert i stron z kodami rabatowymi tam, gdzie są dostępne.