Jak czytać ten przewodnik
mechanizmy, nie lista komendTerraform 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.
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.
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.
- 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 łączyKonfiguracja 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:
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 }
| Blok | Co robi | Kiedy |
|---|---|---|
terraform | wymagane providery i wersje, backend state | raz na moduł główny |
provider | konfiguracja wtyczki: region, uwierzytelnienie | raz na konto/region (aliasy: rozdz. o multi-region) |
resource | obiekt, którym Terraform zarządza: tworzy, zmienia, usuwa | wszystko, co ma żyć i umierać z kodem |
data | obiekt tylko do odczytu | istniejące AMI, VPC innego zespołu, aktualne konto |
variable | wejście modułu | to, co różni środowiska |
locals | nazwane wyrażenie pomocnicze | żeby nie powtarzać wyrażeń |
output | wyjście modułu | przekazanie wartości dalej lub do CLI |
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.
- Katalog
.tf= moduł; kolejność tekstu nie ma znaczenia. resource= zarządzam,data= tylko czytam.variable/outputto interfejs modułu,localsto jego zmienne wewnętrzne.
Provider: Terraform sam nie zna żadnej chmury
rdzeń liczy graf, wtyczka mówi z APITerraform 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.
terraform init.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.
- Core jest niezależny od chmury; całą wiedzę o API wnosi provider.
initpobiera 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.
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.
- 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.
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 ślepyPo 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.
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).
- 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).
apply -refresh-only drift zniknął na zawsze?prod, więc następny zwykły plan znowu zaproponuje zmianę na prod. Refresh-only aktualizuje pamięć, nie kod.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.
- 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-onlyakceptuje rzeczywistość w state; kod musisz poprawić sam.
Backend i blokada: state dla zespołu
jeden plik, dwie osoby, jeden lockBackend 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.
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.
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”.
- 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 terytoriumTrzy bloki w kodzie pozwalają przerabiać state przez zwykły plan i apply — z podglądem i code review — zamiast ręcznych komend:
| Narzędzie | Co robi | Kiedy |
|---|---|---|
import { to = …, id = "…" } | dopisuje istniejący obiekt do state pod adresem | przejmujesz ręcznie stworzone zasoby |
plan -generate-config-out=… | generuje bloki resource dla bloków import | nie chcesz pisać konfiguracji ręcznie |
moved { from = …, to = … } | zmienia adres w state, bez niszczenia | zmiana nazwy, przeniesienie do modułu |
removed { from = …, lifecycle { destroy = false } } | usuwa z state, obiekt zostaje w chmurze | oddajesz zasób innemu zespołowi/narzędziu |
terraform state mv / rm | to samo komendą CLI, bez planu | naprawy awaryjne |
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.
importdopisuje,movedprzenazywa,removedzapomina — 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 plikuState 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.
- Trzymaj state w zdalnym backendzie z szyfrowaniem i kontrolą dostępu; nigdy nie commituj
*.tfstate. - Hasła przekazuj przez argumenty write-only albo zasoby ephemeral, gdy provider je wspiera.
- Jeszcze lepiej: pozwól usłudze wygenerować i przechowywać sekret (np. menedżer sekretów), a Terraform niech zna tylko jego ID.
- State = dane wrażliwe, zawsze.
sensitiveredaguje wyjście CLI, nie state.- Write-only i ephemeral omijają state i plan.
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ą wykonujeterraform 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.
| Opcja plan/apply | Co robi | Kiedy |
|---|---|---|
-out=FILE | zapisuje plan do pliku | review przed apply, CI |
-destroy | plan usunięcia wszystkiego z state | środowiska tymczasowe |
-refresh-only | tylko aktualizacja state do rzeczywistości | akceptacja driftu |
-replace=ADRES | wymusza zastąpienie instancji | obiekt zdegradowany, choć konfiguracja się nie zmieniła |
-target=ADRES | planuje tylko część zasobów | wyjątkowo: naprawy; nie rutynowo |
-parallelism=n | limit równoległych operacji, domyślnie 10 | limity API (throttling) |
initprzygotowuje,planliczy różnicę,applyją wykonuje.- Zapisany plan wykonuje się bez pytania, ale tylko na tym samym state.
-targetto narzędzie awaryjne, nie codzienne.
Graf zależności i równoległość
kolejność z referencji, nie z plikuZ 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.
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.
- Referencje → krawędzie grafu; graf bez cykli.
- Niezależne węzły wykonują się równolegle, do limitu
-parallelism. depends_ontylko dla zależności ukrytych.
Akcje planu: update czy replace?
o tym decyduje API, nie tyDla 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.
example_db jest wymyślony; który atrybut wymusza replace, zależy od konkretnego providera i API.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ę.
+create,~update,-destroy,-/+replace.- Replace wynika ze schematu providera i ograniczeń API.
-replace=ADRESwymusza replace bez zmiany kodu.
lifecycle: sterujesz kolejnością i ochroną
create_before_destroy, prevent_destroy, ignore_changesDomyślnie replace to „najpierw zniszcz, potem utwórz” — przez chwilę obiektu nie ma. Blok lifecycle zmienia to zachowanie:
| Argument | Co robi | Kiedy |
|---|---|---|
create_before_destroy | najpierw nowy obiekt, potem usunięcie starego | brak przerwy, gdy API pozwala na dwa obiekty naraz |
prevent_destroy | błąd przy każdym planie, który niszczy obiekt | bazy, buckety z danymi |
ignore_changes | ignoruje zmiany wskazanych atrybutów | atrybut zmienia ktoś inny legalnie |
replace_triggered_by | replace, gdy zmieni się wskazany zasób/atrybut | wymiana instancji po zmianie zależności |
precondition/postcondition | własne asercje z komunikatem | walidacja założeń |
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.
- Domyślny replace: destroy → create (przerwa).
create_before_destroy: create → destroy; uważaj na unikalne nazwy.prevent_destroydziała tylko, dopóki blok istnieje.
count kontra for_each
indeks przesuwa się, klucz zostajeOba 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:
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).
count: tożsamość = pozycja; usunięcie ze środka przesuwa resztę.for_each: tożsamość = klucz; usuwasz dokładnie jeden obiekt.- Klucze
for_eachmuszą być znane w czasie planu.
Moduły: funkcje z infrastrukturą w środku
variable na wejściu, output na wyjściuModuł 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.
module.…). Dodaj bloki moved { from = aws_vpc.main, to = module.net.aws_vpc.this } — plan pokaże przeniesienie zamiast replace.- Moduł = katalog; interfejs = variable + output.
- Adresy w module mają prefiks
module.X.. - Refaktoryzacja do modułów wymaga bloków
moved(albo ręcznegoterraform state mv).
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/DeleteKaż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.
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.
- 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-cloudCzę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.
- 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 konfiguracjiBlok 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.
provider "aws" { region = "us-east-1" }
provider "aws" {
alias = "eu"
region = "eu-central-1"
}
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.
- Konfiguracja bez
aliasjest 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ędzieChmury 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" }.
- 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ł.
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 bugGdy 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ę.
✅ 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, blokicheck
❌ 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_eachznanych dopiero po apply - nie ukryje sekretów w state przez samo
sensitive
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.
- 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 AWSCloudFormation 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.
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.
- 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 ciebieBicep 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 Terraform | Odpowiednik Bicep/ARM | Różnica |
|---|---|---|
| plik state | stan w ARM | nic do przechowywania ani blokowania |
terraform plan | what-if | podgląd przed wdrożeniem |
| usunięcie z kodu | incremental: nic; deployment stack: usuń lub odłącz | trzeba świadomie wybrać stack |
| provider azurerm/azapi | typy zasobów ARM z wersją API | Bicep zna nowe typy bez czekania na providera |
- 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ć.
Mechanizmy z tego przewodnika — state, graf, plan, providery — są wspólne dla Terraform, OpenTofu i Infra Managera. Uczysz się raz.
- GCP: Deployment Manager wycofany, Infra Manager uruchamia Terraform.
- OpenTofu: otwarty fork Terraform, cel — zgodność.
Egzamin + ściąga
sprawdź, co zostało w głowieŚciąga
| Komenda / blok | Co robi | Kiedy |
|---|---|---|
| terraform init | backend, providery, moduły, lock file | nowy katalog, zmiana providerów/backendu |
| terraform init -upgrade | wybiera najnowsze wersje zgodne z ograniczeniami | świadoma aktualizacja providerów |
| terraform plan -out=tfplan | zapisuje plan | review w CI |
| terraform apply tfplan | wykonuje zapisany plan bez pytania | po zatwierdzeniu planu |
| plan/apply -refresh-only | tylko aktualizacja state | akceptacja driftu |
| plan/apply -replace=ADDR | wymusza replace | uszkodzony obiekt |
| -parallelism=n | limit równoległości, domyślnie 10 | throttling API |
| terraform graph | graf w formacie DOT | debug zależności |
| import { to, id } | przejęcie istniejącego obiektu | zasoby z konsoli |
| moved { from, to } | zmiana adresu bez replace | rename, przeniesienie do modułu |
| removed { from … destroy = false } | usunięcie ze state bez niszczenia | oddanie zasobu |
| lifecycle { create_before_destroy } | nowy przed starym | replace bez przerwy |
| lifecycle { prevent_destroy } | błąd przy planie niszczącym | dane krytyczne |
| lifecycle { ignore_changes } | ignoruje zmiany atrybutów | atrybut zmienia ktoś inny |
| backend "s3" { use_lockfile = true } | blokada state w S3 | zespół na AWS |
| provider = aws.eu | wybór aliasu providera | wiele regionów/kont |
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.