Animowany przewodnik · Terraform

Terraform od środka

Bloki HCL, providery, state, graf zależności, plan i apply — i to, jak jedna składnia zamienia się w wywołania API AWS, Azure i Google Cloud. Plus uczciwa lista tego, czego Terraform nie zrobi, i porównanie z CloudFormation, Bicep i Infrastructure Manager. Przewijaj — diagramy ruszą same.

plan, zanim apply 🧠
przewiń ↓

Jak czytać ten przewodnik

mechanizmy, nie lista komend

Terraform wygląda na prosty: piszesz pliki .tf, wpisujesz terraform apply i chmura się buduje. Kłopoty zaczynają się w dniu, w którym ktoś zmieni coś ręcznie w konsoli, zmienisz nazwę zasobu albo apply wywróci się w połowie. Wtedy liczy się nie to, jakie są komendy, tylko jak Terraform myśli: co porównuje z czym, gdzie to zapisuje i w jakiej kolejności woła API.

Przewodnik ma poziomy. Rozdziały i bloki mają oznaczenia — L1 podstawy, L2 praktyk, L3 ekspert. Głębokość wybierasz przełącznikiem w lewym dolnym rogu; głębszy materiał chowa się, dopóki go nie zechcesz. Mapa poniżej skacze w dowolne miejsce i odhacza przeczytane rozdziały.

🧠 Pomyśl

Usuwasz z kodu blok resource "aws_instance" "web". Kod niczego już nie opisuje. Skąd Terraform wie, że w chmurze jest maszyna do usunięcia — i której dokładnie? Odpowiedź wróci w finale.

Część I

Fundamenty i abstrakcje

Deklaratywny model, bloki HCL, providery i adresy — słownik, którym mówi cała reszta.

Opisujesz cel, nie kroki

skrypt pamięta „zrób”, Terraform pamięta „ma być”

Skrypt w bashu z aws ec2 run-instances jest imperatywny: mówi „utwórz maszynę”. Uruchom go dwa razy — masz dwie maszyny. Terraform jest deklaratywny: mówisz „mają istnieć dwie maszyny o takich parametrach”, a narzędzie liczy różnicę między tym celem a tym, co już istnieje, i wykonuje tylko tę różnicę.

Ta różnica to serce wszystkiego, co dalej. Żeby ją policzyć, Terraform musi znać trzy rzeczy: konfigurację (twoje pliki), state (swoją pamięć o tym, co już zbudował) i rzeczywistość (odpowiedzi API). Plan to wynik porównania tych trzech.

▶ Skrypt kontra Terraform uruchom dwa razy, potem coś zepsuj ręcznie
Skrypt: „utwórz 2 VM”
Terraform: „mają być 2 VM”
Naciśnij ▶.
Uproszczenie: skrypt nie sprawdza, co istnieje; Terraform porównuje cel z tym, co zna. Liczby maszyn ilustracyjne.
❓ Nie ma głupich pytań
P:
Czyli Terraform sam sprawdza całe konto AWS i usuwa, czego nie ma w kodzie?
O:
Nie. Terraform zarządza tylko obiektami, które sam zapisał w state (albo zaimportowałeś). Ręcznie utworzona maszyna, o której state nic nie wie, jest dla niego niewidzialna — nie usunie jej i nie zauważy.
📌 W punktach
  • Konfiguracja opisuje stan docelowy; plan to różnica między celem a tym, co istnieje.
  • Ponowne uruchomienie bez zmian w kodzie i chmurze daje pusty plan: „No changes”.
  • Terraform zarządza tylko tym, co zna ze state.

HCL: siedem bloków, które musisz znać

resource tworzy, data czyta, reszta łączy

Konfiguracja to katalog plików .tf — tak zwany moduł główny (root module). Terraform czyta wszystkie pliki z katalogu naraz; kolejność plików i bloków nie ma znaczenia, bo zależności wynikają z referencji, nie z pozycji w tekście. Kliknij typ bloku:

▶ Anatomia konfiguracji kliknij typ bloku
terraform {
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 6.0" }
  }
  backend "s3" { bucket = "tf-state", key = "app.tfstate", region = "eu-central-1" }
}
provider "aws" { region = "eu-central-1" }
variable "env" { type = string, default = "dev" }
locals { name = "app-${var.env}" }
data "aws_ami" "ubuntu" { most_recent = true, owners = ["099720109477"] }
resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"
  tags          = { Name = local.name }
}
output "ip" { value = aws_instance.web.public_ip }
Wybierz blok.
Uproszczenie: składnia skrócona do jednej linii tam, gdzie HCL wymaga nowych linii między argumentami w bloku; wersja providera i filtr AMI ilustracyjne.
BlokCo robiKiedy
terraformwymagane providery i wersje, backend stateraz na moduł główny
providerkonfiguracja wtyczki: region, uwierzytelnienieraz na konto/region (aliasy: rozdz. o multi-region)
resourceobiekt, którym Terraform zarządza: tworzy, zmienia, usuwawszystko, co ma żyć i umierać z kodem
dataobiekt tylko do odczytuistniejące AMI, VPC innego zespołu, aktualne konto
variablewejście modułuto, co różni środowiska
localsnazwane wyrażenie pomocniczeżeby nie powtarzać wyrażeń
outputwyjście modułuprzekazanie wartości dalej lub do CLI
⚠️ Uwaga

Blok data nigdy niczego nie tworzy ani nie usuwa — Terraform wykonuje na data source tylko operacje odczytu. Jeśli obiekt nie istnieje, zwykle dostaniesz błąd (zależy od data source), a nie nowy obiekt.

📌 W punktach
  • Katalog .tf = moduł; kolejność tekstu nie ma znaczenia.
  • resource = zarządzam, data = tylko czytam.
  • variable/output to interfejs modułu, locals to jego zmienne wewnętrzne.

Provider: Terraform sam nie zna żadnej chmury

rdzeń liczy graf, wtyczka mówi z API

Terraform dzieli się na dwie części: Terraform Core i pluginy (providery). Core czyta konfigurację, trzyma state, buduje graf i plan. Nie wie nic o EC2 ani o sieciach Azure. Tę wiedzę ma provider — osobny program, z którym Core rozmawia przez RPC. Protokół wtyczek jest wersjonowany i zaimplementowany w Protocol Buffers i gRPC.

terraform init czyta required_providers, pobiera pasujące wersje (zwykle z Terraform Registry) do podkatalogu .terraform i zapisuje wybór w pliku .terraform.lock.hcl. Kolejne init wybiorą tę samą wersję, nawet jeśli wyszła nowsza — chyba że dodasz -upgrade.

▶ Core ↔ provider ↔ API init, plan, potem podbij wersję
Terraform Coregraf · state · plan
provider awsnie pobrany
API AWSEC2, IAM, S3…
.terraform.lock.hcl: —
Naciśnij terraform init.
Uproszczenie: numery wersji ilustracyjne; pominięto sumy kontrolne dla wielu platform i lokalne mirrory providerów.
🤓 Głębiej

Lock file zapisuje dla każdego providera wybraną wersję, ograniczenie wersji i sumy kontrolne paczek. Commituj go do repozytorium — dzięki temu CI i koledzy dostaną dokładnie te same binaria. Lock file obejmuje providery, a nie moduły: wersji modułów pilnuje argument version w bloku module.

📌 W punktach
  • Core jest niezależny od chmury; całą wiedzę o API wnosi provider.
  • init pobiera providery i zapisuje wybór w .terraform.lock.hcl — commituj go.
  • Nowa wersja już wybranego providera zwykle tylko przez terraform init -upgrade.

Adresy i referencje: jak Terraform nazywa rzeczy

typ.nazwa[klucz] — to jest tożsamość

Każdy zasób ma adres: aws_instance.web. Pierwsza część to typ zasobu (prefiks aws_ wskazuje providera), druga to nazwa lokalna, którą nadajesz ty. Z count albo for_each dochodzi indeks lub klucz instancji, a w module — prefiks module.nazwa. Adres to tożsamość w świecie Terraform: zmienisz adres — zmienisz tożsamość (rozdział o state pokaże, czym to grozi).

Gdy w argumencie piszesz data.aws_ami.ubuntu.id, tworzysz referencję. Referencje robią dwie rzeczy: podstawiają wartość i rysują krawędź w grafie zależności.

▶ Dekoder adresów wybierz adres
Przykładowe adresy; nazwy lokalne (web, net, ubuntu) wymyślone.
✏️ Zaostrz ołówek

Jaki jest adres drugiej instancji zasobu resource "aws_subnet" "this" z count = 3 w module wywołanym jako module "net"?

Odpowiedź

module.net.aws_subnet.this[1] — indeksy count liczą się od zera.

📌 W punktach
  • Adres = [module.X.]typ.nazwa[indeks|"klucz"]; data. dla data source.
  • Referencja podstawia wartość i tworzy zależność.
  • Zmiana adresu = zmiana tożsamości obiektu.
Część II

State: jak Terraform śledzi zmiany

Pamięć Terraform: co mapuje, jak wykrywa drift, gdzie leży, jak ją bezpiecznie przerabiać i dlaczego jest wrażliwa.

State: mapa z adresów na prawdziwe obiekty

bez state Terraform jest ślepy

Po apply Terraform zapisuje plik state (lokalnie terraform.tfstate, w JSON). Jego główne zadanie to mapowanie: zasób aws_instance.web z konfiguracji to obiekt o ID i-abcd1234 w chmurze. Dodatkowo state przechowuje metadane (np. zależności, żeby wiedzieć, w jakiej kolejności usuwać zasób, którego nie ma już w kodzie) i cache wartości atrybutów.

Teraz widać odpowiedź na pytanie z początku: gdy usuniesz blok z kodu, adres dalej jest w state. Adres w state bez odpowiednika w konfiguracji = do usunięcia.

▶ Konfiguracja · state · chmura apply, zmień nazwę, potem zgub state
Naciśnij apply.
Uproszczenie: jeden zasób, pominięto atrybuty i odświeżanie. ID instancji ilustracyjne.
⚠️ Uwaga

Utrata state nie usuwa infrastruktury — usuwa wiedzę o niej. Następny plan chce wszystko utworzyć od nowa, co kończy się duplikatami albo błędami „już istnieje”. Dlatego state trzymaj w zdalnym backendzie z wersjonowaniem (dokumentacja backendu S3 wprost zaleca włączenie Bucket Versioning).

📌 W punktach
  • State mapuje adres zasobu na ID obiektu w chmurze.
  • Adres w state, którego nie ma w kodzie = plan usunięcia.
  • Zmiana nazwy bez moved = zniszcz i utwórz.

Refresh i drift: gdy chmura zmieni się bez ciebie

plan zaczyna od pytania „jak jest naprawdę?”

Ktoś zmienił tag w konsoli. To drift: rzeczywistość rozjechała się z tym, co mówi kod. Domyślnie terraform plan najpierw odświeża state — czyta aktualne ustawienia obiektów z API — a potem porównuje je z konfiguracją. Wynik: plan, który przywraca wartość z kodu.

Jeśli zmiana ręczna jest właściwa, masz dwie drogi: przepisać ją do kodu albo — gdy chcesz tylko zaktualizować state bez dotykania chmury — użyć trybu terraform plan -refresh-only / apply -refresh-only. Stara komenda terraform refresh robi to samo, ale jest przestarzała (deprecated).

▶ Drift w trzech kolumnach zmień ręcznie, potem wybierz reakcję
Zacznij od ręcznej zmiany.
Uproszczenie: jeden atrybut tags.env; prawdziwy plan pokazuje też inne atrybuty. Wartości ilustracyjne.
❓ Nie ma głupich pytań
P:
Po apply -refresh-only drift zniknął na zawsze?
O:
Zniknął ze state. Konfiguracja dalej mówi prod, więc następny zwykły plan znowu zaproponuje zmianę na prod. Refresh-only aktualizuje pamięć, nie kod.
🤓 Głębiej

ignore_changes to argument bloku lifecycle: Terraform przestaje planować aktualizacje wskazanych atrybutów. Przydaje się, gdy inny system legalnie zmienia atrybut (np. autoskaler zmienia liczbę instancji). -refresh=false pomija odświeżanie — plan jest szybszy, ale nie widzi driftu.

📌 W punktach
  • Plan domyślnie odświeża state z API, potem porównuje z kodem.
  • Drift + zwykły apply = przywrócenie wartości z kodu.
  • -refresh-only akceptuje rzeczywistość w state; kod musisz poprawić sam.

Backend i blokada: state dla zespołu

jeden plik, dwie osoby, jeden lock

Backend decyduje, gdzie leży state: lokalny plik, bucket S3, kontener Azure Storage (azurerm), bucket GCS (gcs) i inne. Konfigurujesz go w bloku terraform { backend "…" {} }, a terraform init go inicjalizuje. HCP Terraform konfiguruje się osobnym blokiem cloud, zamiast backend.

Jeśli backend obsługuje blokady, Terraform blokuje state przy każdej operacji, która może go zapisać — automatycznie. Gdy blokada się nie uda, Terraform nie kontynuuje. W backendzie S3 blokadę włączasz argumentem use_lockfile = true (domyślnie false); starsza blokada przez tabelę DynamoDB (dynamodb_table) jest oznaczona jako przestarzała.

▶ Dwa apply naraz bez blokady i z blokadą
Aladodaje bucket
stateserial 5
Bartekdodaje kolejkę
Wybierz scenariusz.
Uproszczenie: pokazano tylko odczyt i zapis state; numery serial ilustracyjne. Bez blokady wynik zależy od kolejności zapisów.
🤓 Głębiej: state między projektami

Data source terraform_remote_state czyta outputy modułu głównego z najnowszego snapshotu state innej konfiguracji. Wygodne, ale daje czytającemu dostęp do całego snapshotu state. Często lepiej publikować wartości gdzie indziej (np. w parameter store) i czytać je zwykłym data source.

⚠️ Uwaga

terraform force-unlock zdejmuje cudzą blokadę. Używaj tylko wtedy, gdy wiesz, że proces, który ją trzymał, naprawdę nie żyje — inaczej wracasz do scenariusza „bez blokady”.

📌 W punktach
  • Backend = miejsce state; zespół potrzebuje zdalnego backendu.
  • Blokada działa automatycznie, jeśli backend ją wspiera; w S3 włącz use_lockfile.
  • Wersjonuj bucket ze state.

Operacje na state: import, moved, removed

zmieniasz mapę, nie terytorium

Trzy bloki w kodzie pozwalają przerabiać state przez zwykły plan i apply — z podglądem i code review — zamiast ręcznych komend:

NarzędzieCo robiKiedy
import { to = …, id = "…" }dopisuje istniejący obiekt do state pod adresemprzejmujesz ręcznie stworzone zasoby
plan -generate-config-out=…generuje bloki resource dla bloków importnie chcesz pisać konfiguracji ręcznie
moved { from = …, to = … }zmienia adres w state, bez niszczeniazmiana nazwy, przeniesienie do modułu
removed { from = …, lifecycle { destroy = false } }usuwa z state, obiekt zostaje w chmurzeoddajesz zasób innemu zespołowi/narzędziu
terraform state mv / rmto samo komendą CLI, bez planunaprawy awaryjne
▶ Trzy bloki, trzy skutki kliknij operację
Bucket logs istnieje w chmurze, ale Terraform go nie zna.
Uproszczenie: pominięto refresh i atrybuty; nazwy bucketów ilustracyjne.
⚠️ Uwaga

prevent_destroy nie chroni przed skasowaniem bloku. Terraform nie zapisuje tej reguły w state, więc gdy usuniesz konfigurację zasobu, apply zniszczy obiekt mimo prevent_destroy. Chcesz zachować obiekt — użyj removed z destroy = false.

📌 W punktach
  • import dopisuje, moved przenazywa, removed zapomina — wszystko przez plan.
  • Skasowanie bloku bez removed = zniszczenie obiektu.
  • state mv/rm — tylko na awarie, bo omija plan i review.

Sekrety w state: sensitive to nie szyfrowanie

ukryte w konsoli ≠ ukryte w pliku

State zawiera wartości atrybutów — także hasła do baz i tokeny. Lokalnie to zwykły plik tekstowy. sensitive = true na zmiennej lub outputcie tylko redaguje wartość w wyjściu plan/apply; w state wartość dalej jest. Co więcej, terraform output -json i -raw pokazują wartości wrażliwe otwartym tekstem.

Żeby sekret w ogóle nie trafił do state ani do pliku planu, są dwa nowsze narzędzia: argumenty write-only (np. password_wo w aws_db_instance) i zasoby ephemeral (blok ephemeral), których wartości żyją tylko w trakcie operacji.

▶ Gdzie wycieka hasło? wybierz mechanizm
Uproszczenie: „widoczne” = zapisane jawnym tekstem w danym miejscu. Hasło ilustracyjne.
🧰 Przepis: state z sekretami
  1. Trzymaj state w zdalnym backendzie z szyfrowaniem i kontrolą dostępu; nigdy nie commituj *.tfstate.
  2. Hasła przekazuj przez argumenty write-only albo zasoby ephemeral, gdy provider je wspiera.
  3. Jeszcze lepiej: pozwól usłudze wygenerować i przechowywać sekret (np. menedżer sekretów), a Terraform niech zna tylko jego ID.
📌 W punktach
  • State = dane wrażliwe, zawsze.
  • sensitive redaguje wyjście CLI, nie state.
  • Write-only i ephemeral omijają state i plan.
Część III

Wykonanie: od kodu do wywołań API

Cykl init → plan → apply, graf zależności, rodzaje akcji, lifecycle, wiele instancji i moduły.

init → plan → apply

plan to umowa, apply ją wykonuje

terraform init przygotowuje katalog: backend, providery, moduły. terraform plan buduje plan wykonania: odświeża state, porównuje z konfiguracją i wypisuje akcje. terraform apply bez argumentu sam tworzy nowy plan, pyta o zgodę i go wykonuje. W CI lepszy jest tryb dwuetapowy: plan -out=tfplan, review, potem apply tfplan — zapisany plan wykonuje się bez pytania.

Zapisany plan pamięta, z jakiego state powstał (numer serial i lineage). Jeśli w międzyczasie inny apply zmienił state, Terraform odrzuci plan błędem Saved plan is stale.

▶ Potok w CI plan -out, apply, a potem wyścig
Naciśnij ▶.
Uproszczenie: logi skrócone; numery serial ilustracyjne. Komunikat błędu pochodzi z kodu źródłowego Terraform.
Opcja plan/applyCo robiKiedy
-out=FILEzapisuje plan do plikureview przed apply, CI
-destroyplan usunięcia wszystkiego z stateśrodowiska tymczasowe
-refresh-onlytylko aktualizacja state do rzeczywistościakceptacja driftu
-replace=ADRESwymusza zastąpienie instancjiobiekt zdegradowany, choć konfiguracja się nie zmieniła
-target=ADRESplanuje tylko część zasobówwyjątkowo: naprawy; nie rutynowo
-parallelism=nlimit równoległych operacji, domyślnie 10limity API (throttling)
📌 W punktach
  • init przygotowuje, plan liczy różnicę, apply ją wykonuje.
  • Zapisany plan wykonuje się bez pytania, ale tylko na tym samym state.
  • -target to narzędzie awaryjne, nie codzienne.

Graf zależności i równoległość

kolejność z referencji, nie z pliku

Z referencji Terraform buduje graf skierowany: krawędź od zasobu do zasobu, od którego zależy. Graf musi być bez cykli. Potem go „przechodzi” równolegle: węzeł startuje, gdy skończą się wszystkie jego zależności. Liczbę równoczesnych operacji ogranicza -parallelism (domyślnie 10).

Usuwanie idzie w odwrotnej kolejności: najpierw maszyna, potem podsieć, na końcu VPC. Gdy zależność nie wynika z żadnej referencji (np. aplikacja potrzebuje uprawnienia IAM, którego nie widać w argumentach), dopisujesz depends_on — ale tylko w ostateczności, bo robi plany bardziej zachowawczymi.

▶ Przejście grafu zmień parallelism, potem dodaj cykl
Naciśnij ▶ apply.
Uproszczenie: każda operacja trwa tyle samo „taktów” (ilustracyjnie); prawdziwe czasy zależą od API.
🤓 Głębiej

terraform graph wypisuje graf w formacie DOT (Graphviz). Wewnętrznie Terraform buduje osobne grafy dla planu i apply; usuwanie i tworzenie tego samego zasobu mają różną kolejność, więc nie mogą być jednym węzłem.

📌 W punktach
  • Referencje → krawędzie grafu; graf bez cykli.
  • Niezależne węzły wykonują się równolegle, do limitu -parallelism.
  • depends_on tylko dla zależności ukrytych.

Akcje planu: update czy replace?

o tym decyduje API, nie ty

Dla każdego zasobu plan wybiera akcję: create (+), update in-place (~), destroy (-) albo replace (-/+: zniszcz i utwórz). Replace pojawia się, gdy zmieniasz argument, którego zdalne API nie potrafi zmienić na żywym obiekcie. Tę wiedzę ma provider: w jego schemacie atrybut jest oznaczony jako wymagający zastąpienia (w Plugin Framework: plan modifier RequiresReplace). Plan pokazuje wtedy dopisek # forces replacement.

▶ Co zrobi plan? wybierz zmianę w kodzie

    
Ilustracja: zasób example_db jest wymyślony; który atrybut wymusza replace, zależy od konkretnego providera i API.
⚠️ Uwaga

Zawsze czytaj podsumowanie Plan: X to add, Y to change, Z to destroy. Niewinna zmiana nazwy bazy danych może oznaczać -/+ i utratę danych. Na produkcji traktuj każde „to destroy” jak czerwoną lampkę.

📌 W punktach
  • + create, ~ update, - destroy, -/+ replace.
  • Replace wynika ze schematu providera i ograniczeń API.
  • -replace=ADRES wymusza replace bez zmiany kodu.

lifecycle: sterujesz kolejnością i ochroną

create_before_destroy, prevent_destroy, ignore_changes

Domyślnie replace to „najpierw zniszcz, potem utwórz” — przez chwilę obiektu nie ma. Blok lifecycle zmienia to zachowanie:

ArgumentCo robiKiedy
create_before_destroynajpierw nowy obiekt, potem usunięcie staregobrak przerwy, gdy API pozwala na dwa obiekty naraz
prevent_destroybłąd przy każdym planie, który niszczy obiektbazy, buckety z danymi
ignore_changesignoruje zmiany wskazanych atrybutówatrybut zmienia ktoś inny legalnie
replace_triggered_byreplace, gdy zmieni się wskazany zasób/atrybutwymiana instancji po zmianie zależności
precondition/postconditionwłasne asercje z komunikatemwalidacja założeń
▶ Replace na osi czasu porównaj kolejność
Wybierz wariant.
Ilustracja: czasy umowne. Konflikt nazw zależy od API — dotyczy zasobów, których nazwa musi być unikalna.
🤓 Głębiej

Z reguł lifecycle tylko create_before_destroy jest zapisywane w state. Pozostałe Terraform czyta wyłącznie z konfiguracji — stąd pułapka z rozdziału o removed: skasowany blok zabiera ze sobą prevent_destroy.

📌 W punktach
  • Domyślny replace: destroy → create (przerwa).
  • create_before_destroy: create → destroy; uważaj na unikalne nazwy.
  • prevent_destroy działa tylko, dopóki blok istnieje.

count kontra for_each

indeks przesuwa się, klucz zostaje

Oba tworzą wiele instancji z jednego bloku. count numeruje je [0], [1]…, for_each nadaje klucze z mapy lub zbioru: ["a"], ["b"]. Dokumentacja radzi: count dla niemal identycznych instancji, for_each, gdy argumenty różnią się wartościami, których nie da się wyprowadzić z liczby całkowitej. Różnica wychodzi przy usuwaniu elementu ze środka listy:

▶ Usuń „a” z listy [a, b, c] przełącz tryb
Wybierz tryb i usuń „a”.
Uproszczenie: nazwa użytkownika jest jedynym atrybutem; po usunięciu elementu z początku listy count przesuwa indeksy — gdyby zmiana nazwy wymuszała replace, dwa obiekty zostałyby zastąpione, a ostatni zniszczony, zamiast usunąć jeden.
⚠️ Uwaga

Wartości for_each (klucze) muszą być znane podczas planowania. Referencja do atrybutu, który poznamy dopiero po apply — np. ID nadawanego przez API — kończy się błędem. Plan pokazuje takie wartości jako (known after apply).

📌 W punktach
  • count: tożsamość = pozycja; usunięcie ze środka przesuwa resztę.
  • for_each: tożsamość = klucz; usuwasz dokładnie jeden obiekt.
  • Klucze for_each muszą być znane w czasie planu.

Moduły: funkcje z infrastrukturą w środku

variable na wejściu, output na wyjściu

Moduł to katalog plików .tf. Moduł główny wywołuje inne blokiem module "net" { source = "./modules/network" … }. Źródłem może być katalog lokalny, Registry albo repozytorium Git. Zasoby modułu dostają adresy z prefiksem module.net., a wartości wychodzą na zewnątrz tylko przez output.

▶ Drzewo modułów kliknij moduł
Kliknij moduł.
Nazwy modułów i zasobów ilustracyjne.
❓ Nie ma głupich pytań
P:
Przeniosłem zasoby do modułu i plan chce wszystko zniszczyć. Co zrobiłem źle?
O:
Zmieniły się adresy (doszedł prefiks module.…). Dodaj bloki moved { from = aws_vpc.main, to = module.net.aws_vpc.this } — plan pokaże przeniesienie zamiast replace.
📌 W punktach
  • Moduł = katalog; interfejs = variable + output.
  • Adresy w module mają prefiks module.X..
  • Refaktoryzacja do modułów wymaga bloków moved (albo ręcznego terraform state mv).
Część IV

Jak abstrakcje mapują się na chmury

Co provider robi z twoim blokiem, dlaczego „ta sama sieć” w trzech chmurach to trzy różne zasoby, i jak żyć z API, które odpowiada z opóźnieniem.

Od bloku resource do wywołania API

jeden typ zasobu = schemat + Create/Read/Update/Delete

Każdy typ zasobu w providerze to schemat (jakie argumenty i atrybuty, które są wymagane, które wymuszają replace) oraz implementacja operacji Create, Read, Update, Delete. Core decyduje co zrobić (akcja z planu), provider jak: tłumaczy argumenty na żądania do API konkretnej chmury i zapisuje odpowiedź jako atrybuty w state.

Dlatego abstrakcja Terraform kończy się na poziomie cyklu życia: create, read, update, delete, import. Same zasoby nie są abstrakcyjne — aws_vpc ma argumenty EC2, a azurerm_virtual_network argumenty ARM.

▶ Ta sama akcja, trzy API wybierz chmurę i operację
Wybierz operację.
Uproszczenie: pokazano jedno główne wywołanie API na operację; provider zwykle wykonuje kilka (odczyt po utworzeniu, czekanie na gotowość). Ścieżki URL skrócone.
🤓 Głębiej: Azure ma dwa providery

azurerm ma ręcznie zaprojektowane zasoby z wygodnymi argumentami. azapi to cienka warstwa na ARM REST API: pozwala zarządzać dowolnym typem zasobu Azure w dowolnej wersji API — także funkcjami, których azurerm jeszcze nie obsługuje. Płacisz za to mniej wygodnym, „surowym” ciałem żądania.

📌 W punktach
  • Core: co zrobić. Provider: jak to zrobić w danym API.
  • Wspólna jest semantyka cyklu życia, nie model zasobów.
  • Nowa funkcja chmury jest w Terraform dopiero wtedy, gdy ma ją provider.

Ten sam pomysł, trzy różne zasoby

Terraform nie jest warstwą multi-cloud

Częsty mit: „napiszę kod raz i przeniosę między chmurami”. Terraform daje wspólny język i workflow, ale nie wspólny model zasobów. Sieć w AWS jest regionalna, w Google Cloud globalna (podsieci są regionalne), a w Azure każdy zasób musi leżeć w grupie zasobów. Kod trzeba napisać osobno dla każdej chmury.

▶ Słownik pojęć wybierz koncept
Wybrane typy zasobów z providerów hashicorp/aws, hashicorp/azurerm i hashicorp/google; każdy z nich ma też inne, pokrewne zasoby.
🔥 Rozmowa przy kominku
aws_vpcMam swój blok CIDR i żyję w regionie. Podsieć dostaje strefę dostępności.
google_compute_networkJa jestem globalna. Regiony mają dopiero moje podsieci.
azurerm_virtual_networkA ja bez grupy zasobów nawet nie powstanę.
📌 W punktach
  • Wspólny język, workflow i state — różne zasoby.
  • Kontener zakresu: konto+region (AWS), subskrypcja+grupa zasobów (Azure), projekt (GCP).
  • Wspólną fasadę możesz zbudować sam modułami, ale to twoja abstrakcja, nie Terraform.

Uwierzytelnienie, aliasy i wiele regionów

jeden provider, wiele konfiguracji

Blok provider ustawia region, konto i uwierzytelnienie. Sekretów nie wpisuj w kod — providery czytają standardowe źródła poświadczeń chmury (zmienne środowiskowe, profile, tożsamość maszyny/CI). Gdy potrzebujesz dwóch regionów albo dwóch kont, tworzysz drugą konfigurację z argumentem alias i wskazujesz ją w zasobie przez provider = aws.eu.

▶ Routing zasobów do konfiguracji providera kliknij zasób
provider "aws" { region = "us-east-1" }
provider "aws" {
  alias  = "eu"
  region = "eu-central-1"
}
aws (domyślny)us-east-1
aws.eueu-central-1
Kliknij zasób.
Nazwy bucketów ilustracyjne.
🤓 Głębiej

Moduły nie powinny same deklarować bloków provider — moduł dziedziczy domyślne konfiguracje od wywołującego, a aliasy przekazuje się jawnie argumentem providers = { aws = aws.eu } w bloku module. State zapamiętuje, której konfiguracji providera użyto dla zasobu.

📌 W punktach
  • Konfiguracja bez alias jest domyślna.
  • Zasób wybiera alias przez provider = aws.eu.
  • Poświadczenia z otoczenia, nie z kodu.

API, które odpowiada „jeszcze nie”

utworzone ≠ widoczne wszędzie

Chmury to systemy rozproszone. Wiele API jest ostatecznie spójnych: obiekt utworzony przed chwilą może przez moment nie być widoczny dla innego wywołania. Terraform Core tego nie rozwiązuje — robią to providery: ponawiają żądania, czekają na stan „gotowy” i mają limity czasu. Część zasobów pozwala zmienić te limity zagnieżdżonym blokiem timeouts { create = "60m" }.

▶ Rola IAM i jej pierwsze użycie z ponawianiem i bez
Wybierz wariant.
Ilustracja: komunikaty i czasy wymyślone; pokazują wzorzec, a nie zachowanie konkretnego providera.
📌 W punktach
  • Spójność i ponawianie to odpowiedzialność providera.
  • Długie operacje: blok timeouts, jeśli zasób go wspiera.
  • Błąd po timeout nie cofa zmian — zobacz następny rozdział.
Część V

Granice i porównania

Czego Terraform nie robi, i jak to samo rozwiązują natywne narzędzia AWS, Azure i Google Cloud.

Co można, a czego nie

brak rollbacku to cecha, nie bug

Gdy apply trafi na błąd, Terraform loguje błąd, zapisuje w state zmiany, które już się udały, zdejmuje blokadę i kończy. Nie cofa automatycznie częściowo wykonanego apply. Infrastruktura może zostać w stanie pośrednim; poprawiasz kod (albo przyczynę) i uruchamiasz apply ponownie — plan dokończy resztę.

▶ Apply przerwany w połowie Terraform vs rollback stosu
Wybierz scenariusz.
Uproszczenie: cztery niezależne kroki wykonywane po kolei; w Terraform mogłyby iść równolegle.

✅ Terraform potrafi

  • deklaratywnie zarządzać zasobami każdej usługi, dla której jest provider (chmury, DNS, SaaS, Kubernetes…)
  • pokazać plan przed zmianą
  • wykryć drift obiektów, które zna
  • przejąć istniejące zasoby (import) i refaktoryzować (moved)
  • walidować założenia: validation, precondition, bloki check

❌ Terraform nie

  • nie cofa automatycznie nieudanego apply
  • nie widzi zasobów spoza state
  • nie jest narzędziem do konfiguracji systemów operacyjnych — provisionery to ostateczność
  • nie da kluczy for_each znanych dopiero po apply
  • nie ukryje sekretów w state przez samo sensitive
⚠️ Uwaga

Provisionery (local-exec, remote-exec) uruchamiają skrypty po utworzeniu zasobu, ale Terraform nie modeluje ich skutków w planie. Dokumentacja każe wyczerpać wszystkie alternatywy (obrazy maszyn, narzędzia do zarządzania konfiguracją, mechanizmy samego providera) i dopiero w ostateczności sięgać po provisioner.

📌 W punktach
  • Nieudany apply: część zmian zostaje, state je zna, brak rollbacku.
  • Naprawa = poprawka + kolejny apply.
  • Konfiguracja OS i aplikacji — inne narzędzia.

Terraform kontra CloudFormation

plik state u ciebie vs stos po stronie AWS

CloudFormation to natywne IaC AWS. Szablon (JSON/YAML) opisuje zasoby; z szablonu powstaje stos (stack), którego stan trzyma usługa AWS — nie masz pliku state ani backendu. Odpowiednikiem planu jest change set: pokazuje, co się zmieni (także czy zasób zostanie usunięty lub zastąpiony), a zmiany wchodzą dopiero po jego wykonaniu.

Przy niepowodzeniu operacji stos można wycofać do ostatniego stabilnego stanu albo — opcją „Preserve successfully provisioned resources” — zachować udane zasoby i ponowić. Drift wykrywa osobna operacja drift detection; zasoby, które jej nie wspierają, dostają status NOT_CHECKED.

▶ Porównywarka wybierz scenariusz
Uproszczenie: jedno zdanie na narzędzie; szczegóły w tekście rozdziałów. Stan na październik 2026.
🤓 Głębiej

W CloudFormation usunięcie zasobu z szablonu usuwa go przy aktualizacji stosu; atrybut DeletionPolicy: Retain zachowuje obiekt — to odpowiednik removed z destroy = false. Jedna operacja na hierarchii zagnieżdżonych stosów może tworzyć, zmieniać lub usuwać najwyżej 2500 zasobów.

📌 W punktach
  • CFN: stan po stronie AWS, Terraform: plik state w backendzie.
  • Change set ≈ plan; drift detection ≈ refresh, ale na żądanie.
  • CFN tylko AWS; Terraform: każdy provider.

Terraform kontra ARM i Bicep

Azure trzyma stan za ciebie

Bicep to język kompilowany do szablonów ARM. Nie ma plików state: Azure Resource Manager przechowuje stan zasobów, a wdrożenie jest idempotentne. Podgląd zmian daje operacja what-if. Domyślny tryb wdrożenia to incremental: zasoby w grupie, których nie ma w szablonie, zostają nietknięte. Tryb complete by je usunął, ale Microsoft go stopniowo wycofuje i do usuwania poleca deployment stacks.

Różnica w myśleniu: Terraform usuwa to, co zna ze state, a czego nie ma w kodzie. ARM w trybie incremental niczego nie usuwa; dopiero deployment stack śledzi „swoje” zasoby i może je usunąć lub odłączyć, a także zablokować zmiany spoza stosu (denySettings).

Pojęcie TerraformOdpowiednik Bicep/ARMRóżnica
plik statestan w ARMnic do przechowywania ani blokowania
terraform planwhat-ifpodgląd przed wdrożeniem
usunięcie z koduincremental: nic; deployment stack: usuń lub odłącztrzeba świadomie wybrać stack
provider azurerm/azapitypy zasobów ARM z wersją APIBicep zna nowe typy bez czekania na providera
📌 W punktach
  • Bicep: brak state, what-if jako podgląd.
  • Incremental (domyślny) nie usuwa niczego.
  • Usuwanie i ochrona przed zmianami: deployment stacks.

Google Cloud i OpenTofu: gdy natywne = Terraform

Deployment Manager odszedł

Google Cloud Deployment Manager osiągnął koniec wsparcia 31 marca 2026 r.; Google każe migrować do Infrastructure Manager. Infra Manager to zarządzana usługa, która wdraża konfiguracje Terraform (z Cloud Storage, repozytorium Git lub katalogu lokalnego). Natywne IaC w Google Cloud to więc Terraform uruchamiany przez Google, a nie osobny język.

OpenTofu powstał w 2023 r. jako fork Terraform, po tym jak HashiCorp zmienił licencję swoich produktów na Business Source License (BSL). Projekt trafił pod skrzydła Linux Foundation i dąży do zgodności z konfiguracjami Terraform — większość kodu działa bez zmian, ale funkcje dodane po rozejściu się projektów mogą się różnić.

😌 Spokojnie

Mechanizmy z tego przewodnika — state, graf, plan, providery — są wspólne dla Terraform, OpenTofu i Infra Managera. Uczysz się raz.

📌 W punktach
  • GCP: Deployment Manager wycofany, Infra Manager uruchamia Terraform.
  • OpenTofu: otwarty fork Terraform, cel — zgodność.

Egzamin + ściąga

sprawdź, co zostało w głowie
Wynik: 0 / 0

Ściąga

Komenda / blokCo robiKiedy
terraform initbackend, providery, moduły, lock filenowy katalog, zmiana providerów/backendu
terraform init -upgradewybiera najnowsze wersje zgodne z ograniczeniamiświadoma aktualizacja providerów
terraform plan -out=tfplanzapisuje planreview w CI
terraform apply tfplanwykonuje zapisany plan bez pytaniapo zatwierdzeniu planu
plan/apply -refresh-onlytylko aktualizacja stateakceptacja driftu
plan/apply -replace=ADDRwymusza replaceuszkodzony obiekt
-parallelism=nlimit równoległości, domyślnie 10throttling API
terraform graphgraf w formacie DOTdebug zależności
import { to, id }przejęcie istniejącego obiektuzasoby z konsoli
moved { from, to }zmiana adresu bez replacerename, przeniesienie do modułu
removed { from … destroy = false }usunięcie ze state bez niszczeniaoddanie zasobu
lifecycle { create_before_destroy }nowy przed starymreplace bez przerwy
lifecycle { prevent_destroy }błąd przy planie niszczącymdane krytyczne
lifecycle { ignore_changes }ignoruje zmiany atrybutówatrybut zmienia ktoś inny
backend "s3" { use_lockfile = true }blokada state w S3zespół na AWS
provider = aws.euwybór aliasu providerawiele regionów/kont
🎓 Gratulacje

Odpowiedź na pytanie z początku: kod już nie opisuje maszyny, ale state dalej trzyma wpis aws_instance.web → i-…. Adres w state bez odpowiednika w konfiguracji to dla planu akcja destroy — i stąd Terraform wie, którą dokładnie maszynę usunąć. Następny krok: weź małe środowisko, zrób refaktor z moved, przejmij jeden ręczny zasób przez import i przeczytaj każdy plan, zanim go zatwierdzisz.