Po co w ogóle Terraform w pipeline i co to zmienia
Lokalne terraform apply vs uruchomienie w CI/CD
Ręczne uruchamianie terraform apply na lokalnym komputerze działa dopóki zespół jest mały, a środowisk niewiele. Taki model szybko się jednak łamie, gdy kilku inżynierów dotyka tej samej infrastruktury albo pojawia się wymaganie śladu audytowego.
Przy lokalnym podejściu trudno odpowiedzieć na proste pytania: kto, kiedy i na podstawie jakiego commita wykonał zmiany. Gdy coś pójdzie nie tak, odtworzenie sekwencji zdarzeń bywa bolesne. Dodatkowo operujesz lokalnymi credentialami, co komplikuje rotację kluczy i kontrolę dostępu.
Terraform w pipeline CI/CD przenosi odpowiedzialność z pojedynczych laptopów na kontrolowane, powtarzalne środowisko. Każdy plan i apply jest powiązany z konkretną zmianą w repozytorium, logi lądują w jednym miejscu, a uprawnienia są oparte o konta serwisowe, a nie osobiste.
Główne korzyści: powtarzalność, audyt, kontrola dostępu
Automatyzacja Terraforma w pipeline daje trzy kluczowe efekty: powtarzalność, pełny ślad oraz spójny model uprawnień.
Powtarzalność oznacza, że ten sam kod uruchamiany jest w identyczny sposób przy każdym deployu. Brak „magii” na lokalnych maszynach, brak ręcznych kroków, o których wie tylko jedna osoba. Każdy job CI używa tej samej wersji Terraforma, tych samych providerów i tych samych kroków.
Ślad audytowy pojawia się naturalnie. Pipeline rejestruje kto zrobił merge, jaki commit go wyzwolił, jaki był wynik plan i apply. W połączeniu z logami chmury daje to spójną historię zmian infrastruktury.
Kontrola dostępu przenosi się na poziom platformy CI i chmury. Inżynier nie potrzebuje pełnych uprawnień do produkcji na swoim laptopie. Wystarczy, że może tworzyć merge requesty, a same zmiany w chmurze wykonuje techniczne konto pipeline’u z dobrze ograniczonymi rolami.
Typowe motywacje zespołów i kanał migracji
Do przeniesienia Terraforma do CI/CD zwykle pcha praktyka, nie teoria. Najczęstsze motywacje to:
- rosnący zespół i konieczność ujednolicenia sposobu pracy,
- wprowadzenie środowisk test/stage/prod,
- wymogi compliance (audyt, separacja obowiązków),
- chęć skrócenia czasu przeglądu zmian infrastruktury.
Przykładowy scenariusz przejścia wygląda prosto: na początku zespół robi lokalne apply na podstawie master/main. Później pojawia się pomysł, by generować terraform plan w merge requestach i dyskutować o nim w komentarzach. Ostatni krok to przeniesienie apply do pipeline’u, uruchamianego wyłącznie z głównej gałęzi lub po oznaczeniu tagiem.
Ten prosty model „plan w MR, apply po merge” zmienia kulturę pracy. Infrastructure as Code staje się faktycznie kodem, który przechodzi review, testy i kontrolowany deployment, a nie jedynie skryptem uruchamianym z czyjegoś laptopa.
Podstawy Terraforma, które muszą zgrać się z pipeline
Struktura katalogów i modułów: monorepo vs multi-repo
Pipeline z Terraformem będzie tak przejrzysty, jak przejrzysta jest struktura repozytorium. Bałagan w katalogach od razu przekłada się na skomplikowane, kruche joby CI.
Dwa typowe podejścia to monorepo i multi-repo. W monorepo trzymane są wszystkie moduły i konfiguracje środowisk, często z podziałem np. na ./modules i ./envs/prod, ./envs/stage itp. Pipeline może wtedy reagować na zmiany w konkretnych katalogach i uruchamiać tylko potrzebne ścieżki.
W multi-repo każdy system lub większy domenowy fragment infrastruktury ma własne repozytorium, ze swoim pipeline’em. Upraszcza to zależności między zespołami, ale utrudnia centralne governance. Istotne jest, aby w obu podejściach katalogi z konfiguracją środowisk były wyraźnie rozdzielone i miały spójny schemat nazw.
Dobry punkt wyjścia to:
modules/– moduły wielokrotnego użytku, bez bezpośredniegoapply,envs/dev,envs/stage,envs/prod– katalogi „root modules” dla środowisk,- oddzielny katalog dla eksperymentów/POC, który nie trafia do produkcyjnego pipeline’u.
Backend zdalny i blokada stanu
Pipeline z Terraformem wymaga zdalnego backendu stanu. Plik terraform.tfstate na lokalnym dysku to przepis na konflikty i nadpisywanie zmian. Zdalny backend (S3, GCS, AzureRM, Terraform Cloud) rozwiązuje ten problem i pozwala na blokadę stanu.
Przykładowa konfiguracja backendu S3 z blokadą w DynamoDB:
terraform {
backend "s3" {
bucket = "tf-state-prod"
key = "network/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "tf-state-locks"
encrypt = true
}
}Blokada stanu zapobiega równoczesnemu apply z dwóch pipeline’ów. Jeden job uzyskuje lock, drugi czeka lub kończy się błędem, zależnie od konfiguracji. W praktyce unikniesz w ten sposób trudnych do odtworzenia konfliktów, gdy dwóch inżynierów trafi w to samo okno deployu.
Workspaces czy osobne katalogi dla środowisk
Dla wielu zespołów naturalne jest użycie workspaces Terraforma. Umożliwia to rozdzielenie stanu środowisk przy zachowaniu tego samego kodu. Jednak w pipeline często wygodniej jest mieć osobne katalogi envs/dev, envs/prod i osobny backend dla każdego.
Workspaces sprawdzają się, gdy:
- infrastruktura środowisk jest niemal identyczna,
- pipeline jest prosty, a liczba środowisk niewielka,
- zespół dobrze rozumie ograniczenia i ryzyka workspaces.
Osobne katalogi i backendy są czytelniejsze w większych organizacjach. Każde środowisko ma własny stan, własny pipeline i własne uprawnienia. Daje to mniej niespodzianek, gdy ktoś przypadkiem użyje złego workspace w lokalnym środowisku.
Standardowa sekwencja kroków i mapowanie na pipeline
Podstawowa sekwencja Terraforma to: init → validate → plan → apply. W pipeline warto rozbić ją na kilka logicznych etapów:
- job formatowania i lintów –
terraform fmt -check, tflint, skanery bezpieczeństwa, - job walidacji –
terraform init -backend=falseiterraform validatedla szybkiego feedbacku, - job plan – pełne
terraform initze zdalnym backendem iterraform plan, - job apply – wymagający ręcznego zatwierdzenia lub warunku na gałąź.
Ważne, aby apply nie wykonywał ponownego plan bez powiązania z poprzednim krokiem. Bezpośrednie terraform apply plan.tfplan na artefakcie z jobu plan zapewnia spójność między tym, co zostało zrecenzowane, a tym, co faktycznie jest wdrażane.
Projektowanie przepływu: od commita do zastosowania zmian
Ogólny schemat: push/merge → walidacja → plan → review → apply
Zdrowy workflow z Terraformem w CI/CD układa się zwykle w podobny schemat, niezależnie od narzędzia.
Najprostszy, a jednocześnie praktyczny model:
- Developer robi zmianę w modułach lub konfiguracji środowiska.
- Tworzy branch i otwiera merge request / pull request.
- Automatycznie uruchamia się lint/validate oraz
terraform plandla danego środowiska lub zestawu środowisk. - Plan jest publikowany jako artefakt lub komentarz do MR/PR.
- Zespół przegląda kod i wynik planu. Jeśli wszystko jest akceptowalne – merge.
- Merge do głównej gałęzi wyzwala
apply, często poprzedzony manualnym zatwierdzeniem dla produkcji.
Taki przepływ wymusza myślenie o planie jako kontrakcie. Kod nie trafia do produkcji, jeśli ktoś nie obejrzał skutków w terraform plan. Zwiększa to bezpieczeństwo, ale nadal pozwala utrzymać dobre tempo dostarczania.
Różne warianty: automatyczny apply niżej, manualny wyżej
Jednym z częstszych kompromisów jest różnicowanie polityki między środowiskami. Dev i stage mogą mieć automatyczny apply po merge, a produkcja – wyłącznie manualny job z zatwierdzeniem.
Typowy model:
- dev – apply automatyczny z gałęzi develop, bez dodatkowych approvals,
- stage – apply automatyczny po merge do main, ale z obowiązkowym review MR,
- prod – apply tylko po specjalnym tagu (np.
prod-release-2023-09-15) lub manualnym uruchomieniu jobu.
Taki podział zmniejsza tarcie w codziennej pracy (szybki feedback na dev/stage), a jednocześnie wprowadza barierę dla produkcji. Przy większych zespołach można dorzucić dodatkowe zasady, np. wymagany approval z innego działu, „change window” czasowe czy powiadomienia na dedykowany kanał komunikatora.
Powiązanie gałęzi Git ze środowiskami
W pipeline z Terraformem warto jasno zdefiniować, które gałęzie kontrolują jakie środowiska. Chaos w tym obszarze generuje najboleśniejsze błędy: przypadkowe apply na produkcję z eksperymentalnego brancha.
Przykładowy, prosty układ:
- branched
feature/*– tylko lint/validate, bez planu na realnych backendach, develop– plan i apply dla środowiska dev,release/*– plan i ewentualnie apply na stage,main– plan i apply produkcyjny, z dodatkowymi protekcjami.
Alternatywnie każdemu środowisku można przypisać osobne repozytorium, dzięki czemu ryzyko pomyłki jest mniejsze, ale rośnie liczba repo i pipeline’ów do utrzymania.
Wybór narzędzia CI/CD a Terraform
Terraform działa poprawnie na większości platform CI/CD. Różnice pojawiają się głównie w ergonomii konfiguracji, obsłudze artefaktów i mechanizmach ochrony środowisk.
| Platforma | Typowa integracja Terraforma | Cechy istotne dla kontroli zmian |
|---|---|---|
| GitHub Actions | Workflow YAML w repo, akcje społeczności | Environment protection, approvals, komentarze do PR |
| GitLab CI | .gitlab-ci.yml, artifacts, environments | Manual jobs, rules, review apps, protected branches |
| Azure DevOps | Pipelines YAML, Service Connections | Approvals & checks, variable groups, RBAC |
| Jenkins | Jenkinsfile, pluginy, własna orkiestracja | Duża elastyczność kosztem większej ilości pracy własnej |
W kontekście Terraforma szczególnie ważne są: łatwe przechowywanie artefaktów (np. planów), dobre mechanizmy ochrony środowisk (manual approvals, protected environments) oraz wygodne zarządzanie sekretami (vault, secret store, encrypted variables).
Walidacja kodu Terraform w pipeline (lint, fmt, validate, testy)
Podstawowy etap: terraform fmt i terraform validate
Najtańsza warstwa obrony to formatowanie i walidacja syntaktyczna. terraform fmt -check dba o spójny styl kodu, a terraform validate weryfikuje, czy konfiguracja jest poprawna względem providerów i modułów.
Przykładowy fragment jobu w dowolnym CI:
terraform init -backend=false
terraform fmt -check
terraform validateWyłączenie backendu w init (-backend=false) pozwala pominąć konfigurację zdalnego stanu na tym etapie. Walidacja jest szybka i nie potrzebuje uprawnień do konta chmurowego – wystarczą zależności providerów pobrane z registry.
Ten etap ma działać dla każdej gałęzi, także feature. Pozwala to zatrzymać błędy składniowe, brakujące zmienne czy literówki w blokach resource zanim w ogóle zespół zobaczy plan.
Statyczna analiza: tflint, checkov, tfsec
Na kolejnej warstwie opłaca się dodać skanery regułowe. tflint dobrze wychwytuje błędy specyficzne dla providerów (np. niepoprawne wartości atrybutów), a tfsec czy Checkov skupiają się na aspektach bezpieczeństwa i zgodności.
Przykładowe reguły, które dają szybki zwrot:
- blokady bucketów S3/GCS przed publicznym dostępem,
- wymóg szyfrowania dysków i baz danych,
Testy jednostkowe i integracyjne dla modułów Terraform
Dla bardziej złożonych modułów sama walidacja syntaktyczna i skanery regułowe są zbyt słabe. Przydają się testy automatyczne, które sprawdzą, czy moduł tworzy zasoby zgodnie z oczekiwaniami.
Dwa popularne podejścia:
- Terratest (Go) – prawdziwe uruchomienie modułu i asercje na żywej infrastrukturze,
- konstrukcje typu „plan-only tests” – analiza wyjścia z
terraform plan(np. w Go, Pythonie) bez faktycznego tworzenia zasobów.
Terratest sprawdza się szczególnie dla modułów krytycznych: VPC, sieć, IAM, bazy danych. Test może np. utworzyć zasoby w tymczasowym projekcie, zweryfikować parametry (otwarte porty, klasy maszyn), a następnie je usunąć.
W pipeline takie testy nie muszą odpalać się na każdą gałąź. Często wystarcza uruchamianie ich przy zmianach w katalogu modules/ lub przy release modułu.
Polityki i kontrola zgodności: Sentinel, OPA, Conftest
W dużych organizacjach pojawia się potrzeba egzekwowania polityk: kto może tworzyć publiczne IP, jakie regiony są dozwolone, jakie typy maszyn wolno używać. Tu wchodzą narzędzia typu Sentinel (komercyjny w ekosystemie HashiCorp) albo OPA/Rego z Conftest.
Typowy schemat użycia OPA/Conftest:
terraform plan -out=plan.tfplanterraform show -json plan.tfplan > plan.jsonconftest test plan.jsonz zestawem reguł Rego.
Reguły mogą blokować plan, jeśli np. detekują publiczny bucket, otwarte 0.0.0.0/0 na porcie 22 lub brak tagów kosztowych. Tego typu checki dobrze działają jako „bramka” przed merge do głównych gałęzi.
Plan jako artefakt i element review
terraform plan staje się w pipeline głównym obiektem dyskusji: co dokładnie ulegnie zmianie i czy jest na to zgoda.
Żeby miało to sens, plan musi być:
- powtarzalny – brak dryfu i losowości w kodzie,
- dostępny – dołączony do MR/PR jako komentarz lub artefakt,
- trwały – ten sam plik użyty później w
apply.
Dobry nawyk to generowanie dwóch form planu:
- ludzkiej – tekst w komentarzu do MR (diff zasobów),
- maszynowej –
plan.tfplanjako artefakt do późniejszegoapply.
Idempotentny i deterministyczny plan
Jeśli plan za każdym razem pokazuje zmiany, choć nic nie zostało realnie zmodyfikowane, zaufanie do narzędzia spada. Typowe przyczyny:
- użycie
random_*bez prawidłowego stanu, - atrybuty liczone po stronie providera, które nie są poprawnie ignorowane (
lifecycle ignore_changes), - brak utrwalonego formatu danych (np. różne kolejności elementów w listach/mapach).
W pipeline dobrze jest mieć kontrolę, czy plan jest „czysty”. Prosty sposób: okresowe uruchomienie jobu plan bez zmian w kodzie i monitorowanie, czy pojawiają się delty. Jeśli tak – trzeba dopracować moduły.
Plan z parametrami środowiskowymi
Konfiguracja środowisk zwykle opiera się na plikach *.tfvars lub zmiennych zewnętrznych. W pipeline trzeba jasno wskazać, z jakimi parametrami generowany jest plan.
Typowy wzór:
terraform plan
-var-file="envs/dev.tfvars"
-out="plan-dev.tfplan"Dla produkcji można użyć innego pliku oraz innych źródeł zmiennych (sekrety z vaulta, parametryzacja per projekt). Kluczowe jest, żeby plan i apply używały dokładnie tego samego zestawu wejść.
Plan jako „kontrakt” z innymi zespołami
W firmach, gdzie kilka zespołów dotyka tej samej infrastruktury, plan przydaje się jako materiał do uzgodnień. Przykład: zespół aplikacyjny dodaje nową usługę, ale zespół platformowy odpowiada za sieć i bezpieczeństwo.
Plan można wtedy:
- dołączyć do MR w repo infrastruktury,
- wysłać na dedykowany kanał (Slack, Teams) przy użyciu webhooka,
- zintegrować z systemem change management jako załącznik.
Zespół platformowy nie musi wtedy czytać każdego pliku .tf, wystarczy, że przejrzy wynikowy diff zasobów.

Plan jako centralny punkt kontroli zmian
Łączenie planu i apply jednym potokiem
Bezpieczny wzorzec: pipeline na gałęzi głównej generuje plan, zatrzymuje się, ktoś go akceptuje, a następny etap wykonuje terraform apply plan.tfplan.
Technicznie wymaga to:
- przechowania
plan.tfplanjako artefaktu między jobami, - odwołania się w jobie apply dokładnie do tego artefaktu,
- blokady ponownego uruchamiania planu w jobie apply.
Dzięki temu nie ma ryzyka, że w międzyczasie ktoś wstrzyknie zmianę i apply pójdzie na inny zestaw modyfikacji niż ten, który został zrecenzowany.
Różne poziomy surowości analizy planu
W małym zespole często wystarczy „czy plan wygląda rozsądnie”. W większych organizacjach stopień szczegółowości rośnie.
Możliwe poziomy:
- prosty review przez inżyniera z doświadczeniem w danym obszarze,
- review krzyżowy – autor + ktoś z innego zespołu (np. bezpieczeństwo),
- automatyczne bramki: policy-as-code, limity na liczbę usuwanych zasobów, blokada przy zmianach w krytycznych modułach.
Dobrym kompromisem jest wymóg dodatkowego review, jeśli plan zawiera usunięcia zasobów produkcyjnych lub zmiany w obszarze sieci / IAM.
Ograniczanie destrukcyjnych zmian
Nic tak nie psuje nastroju, jak przypadkowe usunięcie bazy danych przez nieuważny apply. Wokół planu można zbudować mechanizmy, które takie sytuacje utrudnią.
Przykłady zabezpieczeń:
lifecycle { prevent_destroy = true }dla krytycznych zasobów,- polityki OPA/Sentinel blokujące plany zawierające określone typy destrukcji,
- job CI, który zlicza
-/+i-w planie i wymusza dodatkowy approval przy przekroczeniu progu.
W praktyce często stosuje się regułę: „jeśli plan usuwa zasoby w produkcji, wymagany jest approval co najmniej dwóch osób”.
Plan drift-only i plan per katalog
Poza planem uruchamianym przy zmianie kodu przydaje się też plan periodyczny, który wykrywa dryf (ręczne zmiany w konsoli chmurowej).
Taki job:
- odpala się np. raz dziennie lub raz na tydzień,
- robi
terraform planna aktualnym mainie, - raportuje różnice na kanał zespołu lub w systemie ticketowym.
W większych repo, gdzie katalogów jest wiele (envs/dev/network, envs/dev/app itd.), zamiast jednego dużego planu warto tworzyć kilka mniejszych – jeden na katalog. Ułatwia to analizę i rozbijanie odpowiedzialności między zespołami.
Bezpieczne apply w pipeline: kto i kiedy decyduje
Manualne „gate’y” i approvals
Kluczowa decyzja: kto naciska „apply” i na jakiej podstawie. W CI/CD sprawdza się model, w którym apply jest osobnym jobem z manualnym wyzwoleniem lub wymaganym approvalem.
Typowa konfiguracja:
- job
plandziała automatycznie po merge do main, - job
applyjest oznaczony jako manual i przypięty do protected environment (np.production), - uprawnienia do zatwierdzania mają tylko wybrane role (SRE, Tech Lead).
Dzięki temu merge kodu nie oznacza jeszcze wdrożenia. Jest to szczególnie użyteczne, gdy wdrożenia produkcyjne odbywają się w oknach zmian.
Automatyczny apply w środowiskach niskiego ryzyka
Dev i test rzadko wymagają takiej samej dyscypliny jak produkcja. Tam można pozwolić sobie na pełne auto-deploye.
Przydatne zasady:
- automatyczny
applyz branchy przypisanych do tych środowisk, - limity – np. brak możliwości usuwania zasobów poza pewnym katalogiem,
- szybkie odtworzenie – definicje zasobów tymczasowych, które mogą zostać bez żalu usunięte.
W praktyce taki model pomaga wykrywać błędy Terraform / modułów wcześniej, zanim dotkną one produkcji.
Staged apply: podział na warstwy
Duże zestawy zmian lepiej dzielić na mniejsze partie. Zamiast jednego jobu apply, który zmienia wszystko, stosuje się kilka warstw:
- najpierw sieć i IAM,
- potem bazy danych i kolejki,
- na końcu warstwa aplikacyjna.
Technicznie można to zrealizować przez osobne katalogi / moduły i osobne joby apply, wyzwalane w odpowiedniej kolejności. Ułatwia to roll-back (często wystarczy apply poprzedniego commitów dla wybranej warstwy) i diagnostykę problemów.
Roll-back i roll-forward zamiast cofania apply
Terraform nie ma przycisku „undo”. Cofanie zmian odbywa się właściwie przez kolejne apply z inną konfiguracją.
W pipeline działa to dobrze, jeśli:
- gałąź główna jest zawsze deployowalna,
- starszy stan infrastruktury można odtworzyć przez checkout poprzedniego commita i
apply, - backups (np. DB snapshots) są częścią procesu zmian.
W wielu przypadkach prostsze jest roll-forward: szybkie naprawienie błędu nowym commitem i apply, niż „odwijanie” wszystkiego do stanu sprzed zmiany.
Apply sterowany tagami lub release’ami
Dla produkcji wygodny jest model, w którym apply uruchamia się tylko dla oznaczonych tagów lub release’ów, a nie każdego merge.
Przykład:
- merge do main aktualizuje definicje infrastruktury i generuje plan,
- tag
infra-prod-YYYYMMDD-HHMMwyzwala pipeline z jobem apply na produkcję, - opis taga zawiera link do MR i planu.
Taki przepływ dobrze współgra z formalnymi procesami change management – tag/release może być powiązany z biletem w narzędziu ITSM.
Przykładowe implementacje pipeline z Terraform (GitHub Actions, GitLab CI)
GitHub Actions – prosty workflow z planem w PR
Dla GitHuba popularny jest scenariusz: na każde otwarcie lub update PR uruchamia się workflow, który robi fmt, validate i plan, a wynik wkleja w komentarz.
Minimalny szkic workflow (upraszczający, bez sekcji secrets):
name: Terraform
on:
pull_request:
paths:
- 'infra/**.tf'
- 'infra/**.tfvars'
jobs:
plan:
runs-on: ubuntu-latest
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- name: Terraform init (no backend)
run: terraform init -backend=false
- name: Terraform fmt
run: terraform fmt -check
- name: Terraform validate
run: terraform validate
- name: Terraform init (with backend)
run: terraform init
- name: Terraform plan
run: terraform plan -no-color -out=plan.tfplan > plan.txt
- name: Upload plan artifact
uses: actions/upload-artifact@v4
with:
name: tf-plan
path: infra/plan.tfplan
- name: Comment plan on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
path: infra/plan.txtOsobny workflow może reagować na merge do main i wykonywać terraform apply plan.tfplan, pobierając artefakt wygenerowany wcześniej lub generując nowy plan już po merge, ale z dodatkowymi bramkami (approvals na environment).
GitHub Actions – apply z ochroną środowiska
GitHub pozwala przypisać job do environment (np. production) z wymogiem approval przed startem.
Fragment jobu apply:
jobs:
apply-prod:
runs-on: ubuntu-latest
environment:
name: production
url: https://console.cloud-provider.example
needs: [plan]
if: github.ref == 'refs/heads/main'
defaults:
run:
working-directory: infra
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.
GitHub Actions – matryca środowisk i katalogów
Przy wielu katalogach i środowiskach lepiej zautomatyzować rozbicie pipeline’u na mniejsze jednostki. GitHub Actions pozwala użyć matrix, żeby jednym workflow obsłużyć kilka kombinacji.
jobs:
plan:
runs-on: ubuntu-latest
strategy:
matrix:
env: [dev, stage]
dir: [network, app]
defaults:
run:
working-directory: infra/envs/${{ matrix.env }}/${{ matrix.dir }}
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.0
- name: Terraform init
run: terraform init
- name: Terraform fmt
run: terraform fmt -check
- name: Terraform validate
run: terraform validate
- name: Terraform plan
run: terraform plan -no-color
-out=tfplan-${{ matrix.env }}-${{ matrix.dir }}.bin
> plan-${{ matrix.env }}-${{ matrix.dir }}.txt
- name: Upload plan artifact
uses: actions/upload-artifact@v4
with:
name: tf-plan-${{ matrix.env }}-${{ matrix.dir }}
path: |
tfplan-${{ matrix.env }}-${{ matrix.dir }}.bin
plan-${{ matrix.env }}-${{ matrix.dir }}.txt
W analogiczny sposób można zbudować job apply, który odpali się tylko dla wybranego środowiska (np. przez warunek na tag/branch) i ściągnie odpowiedni artefakt planu.
GitLab CI – klasyczny podział na plan i apply
W GitLab CI dobrze sprawdza się prosty podział na dwa stage’e: plan i apply, z manualnym wyzwoleniem tego drugiego dla produkcji.
stages:
- plan
- apply
variables:
TF_ROOT: "infra"
.plan_template: &plan_template
image: hashicorp/terraform:1.6.0
stage: plan
script:
- cd $TF_ROOT/$TF_DIR
- terraform init
- terraform fmt -check
- terraform validate
- terraform plan -no-color -out=tfplan.bin > plan.txt
artifacts:
paths:
- $TF_ROOT/$TF_DIR/tfplan.bin
- $TF_ROOT/$TF_DIR/plan.txt
expire_in: 3 days
.plan_dev:
<<: *plan_template
variables:
TF_DIR: "envs/dev"
only:
- merge_requests
.plan_prod:
<<: *plan_template
variables:
TF_DIR: "envs/prod"
only:
- main
Tak zbudowany plan można wykorzystać w kolejnym jobie apply, który będzie wymagał manualnego odpalenia i odpowiednich uprawnień.
GitLab CI – apply z protected environment
Apply dla produkcji warto przypisać do environment oznaczonego jako protected. W połączeniu z protected branch daje to sensowną kontrolę.
apply_prod:
image: hashicorp/terraform:1.6.0
stage: apply
needs: ["plan_prod"]
variables:
TF_DIR: "envs/prod"
environment:
name: production
url: https://console.cloud-provider.example
when: manual
allow_failure: false
only:
- main
script:
- cd $TF_ROOT/$TF_DIR
- terraform init
- terraform apply -auto-approve tfplan.bin
W praktyce GitLab potrafi też wymusić approvals przed uruchomieniem joba manualnego na danym environment, co łączy się z procesem change management.
GitLab CI – dynamiczne katalogi po zmianach
Przy monorepo z wieloma katalogami lepiej nie odpalać planu wszędzie. Proste podejście to użycie rules:changes i kilku jobów odpowiadających konkretnym katalogom.
plan_dev_network:
<<: *plan_template
variables:
TF_DIR: "envs/dev/network"
rules:
- changes:
- infra/envs/dev/network/**/*
when: always
- when: never
plan_dev_app:
<<: *plan_template
variables:
TF_DIR: "envs/dev/app"
rules:
- changes:
- infra/envs/dev/app/**/*
when: always
- when: never
Dzięki temu MR zmieniający tylko sieć w dev uruchomi jeden konkretny plan, a nie serię niepotrzebnych jobów.
Zdalny stan, sekrety i uprawnienia – fundament bezpieczeństwa
Wspólny, zdalny state jako warunek sensownego pipeline’u
Bez zdalnego state’u pipeline z Terraformem jest półśrodkiem. Każda maszyna CI musi widzieć aktualny stan, inaczej plan stanie się fikcją.
Typowe backendy:
- Terraform Cloud / Terraform Enterprise,
- bucket (S3, GCS, Azure Blob) + lock w DynamoDB/Cloud Spanner/Blob lease,
- inne serwisy z lockiem (np. Consul).
Dla każdego środowiska i/lub katalogu warto mieć osobny plik state’u i osobny mechanizm blokady. Ułatwia to równoległe apply i ogranicza zasięg ewentualnej korupcji state’u.
Rozdzielenie uprawnień do odczytu i zapisu state’u
Nie każdy pipeline musi mieć prawa do modyfikacji produkcji. Część jobów (lint, validate, plan-drift) może działać tylko z read-only dostępem do state’u i chmury.
Przykładowy podział:
- konto CI plan-only – dostęp
GetObject do bucketa ze state’em, ReadOnly w chmurze, - konto CI apply – pełne uprawnienia do zasobów danego środowiska, przypisane wyłącznie do jobów
apply.
Jeśli backendem jest Terraform Cloud, analogiczny podział osiąga się przez workspaces i role (np. Plan only vs Plan & Apply).
Przechowywanie sekretnych wartości
Zmienne takie jak hasła do baz czy klucze API nie powinny lądować w Git. Nawet zaszyfrowane pliki z tfvars bywają kłopotliwe, bo wymagają zarządzania kluczem.
Praktyczne opcje:
- systemy typu Vault / Secret Manager / Parameter Store i czytanie sekretów przez data source’y Terraforma,
- secrets store w platformie CI (GitHub Secrets, GitLab Variables) i przekazywanie ich jako
TF_VAR_* lub przez plik tfvars generowany w locie, - podział: „publiczne” wartości konfiguracyjne w repo, „tajne” w managerze sekretów.
Dobrym nawykiem jest zakaz używania sensitive outputów w kontekście logów pipeline’u. Nawet jeśli Terraform je maskuje, logi potrafią wyciec w inne miejsca (np. zewnętrzne systemy zbierające metryki jobów).
Minimalne uprawnienia w chmurze
Account CI nie powinno mieć praw do całej subskrypcji czy konta chmurowego. Wystarczy wywoływać API dla konkretnego projektu, resource group albo folderu.
Prosty wzorzec:
- osobne konta / role per środowisko (dev, stage, prod),
- scope roli ograniczony do konkretnego folderu/projektu,
- brak dostępu do manualnego logowania na konto CI (tylko non-interactive).
W AWS można to osiągnąć przez osobne role IAM z trust policy pozwalającą tylko na AssumeRole z konta CI. W Azure – przez managed identity dla agenta lub service principal z odpowiednim scope.
Bezpieczny dostęp CI do backendu state’u
Backend często trzyma się w innym miejscu niż reszta infrastruktury. Dostęp do niego powinien być ograniczony i audytowany.
Przykład dla S3 + DynamoDB:
- bucket na state tylko z
aws:PrincipalArn wskazującym na role CI, - blokada publicznego dostępu, wymuszone szyfrowanie SSE,
- CloudTrail/KMS logs do śledzenia, kto czyta lub modyfikuje pliki.
Analogiczne mechanizmy istnieją w GCP/Azure – najprościej zacząć od polityk bucketów/blobs oraz ról opartych na tożsamości agenta CI.
Kontrola zakresu zmian: drift, selektywne apply, ochrona krytycznych zasobów
Monitoring dryfu infrastruktury
Regularne plany „drift-only” pomagają łapać ręczne zmiany w konsoli. Ich wynik nie musi niczego zmieniać – ma tylko raportować różnice.
Prosty pattern:
- periodic pipeline (cron) odpalany raz dziennie lub tygodniowo,
- dla każdego katalogu / workspace’u:
terraform plan -detailed-exitcode, - przy exit code 2 – zgłoszenie do Slacka / ticket w systemie.
W dużych organizacjach takie joby często zasilają raporty compliance: czy ktoś nie tworzy zasobów poza Terraformem albo nie zmienia ręcznie security group.
Selektywne apply po katalogach i modułach
Terraform działa na aktualnym katalogu, co naturalnie sprzyja podziałowi na mniejsze kawałki. Monolityczne repo z jednym main.tf szybko staje się problemem w CI.
Pragmatyczne podejście:
- podział na katalogi per domena (network, data, app) lub per produkt,
- plan/apply wywoływany tylko dla katalogów, które zmienił MR,
- osobne state’y per katalog i środowisko.
Jeśli moduł jest współdzielony przez wiele środowisk, można użyć terraform workspace zamiast katalogów, ale pipeline musi wtedy pilnować, żeby nie pomieszać workspace’ów w jednym jobie.
Selektory zasobów i targetowanie z umiarem
terraform apply -target=... bywa kuszący, ale łatwo nim wprowadzić chaos. Jeżeli pipeline używa -target, to na jasno zdefiniowane scenariusze, a nie jako stałe obejście.
Typowe, sensowne użycia:
- awaryjne odtworzenie pojedynczego zasobu (np. topicu w kolejce),
- stopniowe wdrażanie nowego modułu, gdy pełny apply jest zbyt ryzykowny.
Po takim „częściowym” apply dobrze jest jak najszybciej wrócić do pełnego planu/apply, żeby state i realna infrastruktura nie rozjechały się logicznie.
Ochrona krytycznych zasobów na poziomie kodu
prevent_destroy = true to podstawa. Powinien pojawić się przy bazach, bucketach z danymi produkcyjnymi, krytycznych kolejkach i wszędzie tam, gdzie przypadkowe usunięcie oznacza incydent.
Przykład modułu DB:
resource "aws_db_instance" "this" {
# ...
lifecycle {
prevent_destroy = true
ignore_changes = [deletion_protection]
}
}
Takie ustawienie wymusza świadomą decyzję: żeby usunąć DB, ktoś musi zdjąć blokadę w kodzie, zreviewować MR i dopiero wtedy wykonać apply.
Policy-as-code jako twarda bramka na plan
Ręczny review planu nie zawsze wystarcza. Narzędzia typu OPA (Conftest), Sentinel, Checkov czy tfsec pozwalają zakodować reguły, które blokują destrukcyjne plany.
Przykładowe reguły:
- zakaz tworzenia publicznych bucketów w produkcji,
- zakaz usuwania RDS w środowisku prod,
- limit liczby zasobów typu
aws_iam_policy modyfikowanych w jednym planie.
Integracja w pipeline jest prosta: po wygenerowaniu planu (najlepiej w JSON) dodaje się krok z analizą i przerwaniem joba, jeśli reguły nie przejdą.
Limity na rozmiar zmian w jednym wdrożeniu
Nawet jeśli zmiany są „bezpieczne”, zbyt duży plan bywa ryzykowny i trudny do przeanalizowania. Da się temu zaradzić prostym licznikiem zmian.
Przykładowy krok w CI:
terraform show -json tfplan.bin > plan.json
python scripts/check_plan_size.py plan.json
Skrypt może np. blokować plan, w którym jest więcej niż określona liczba - (destroy) lub łączna suma zmian przekracza przyjęty próg. Wtedy trzeba podzielić MR na mniejsze.
Kwarantanna dla nowych typów zasobów
Nowe klasy zasobów często wymagają dodatkowego review bezpieczeństwa lub architektury. Można to wymusić na poziomie pipeline’u.
Praktyczne podejście:
- reguła policy-as-code wykrywająca pojawienie się nowego typu (np. nowego rodzaju bazy),
- oznaczenie MR etykietą (np.
needs-arch-review) i blokada auto-apply, - po zatwierdzeniu – aktualizacja reguły, żeby kolejne użycia nie wymagały dodatkowego procesu.
Taki mechanizm pomaga panować nad tym, jakie usługi chmurowe realnie funkcjonują w organizacji i kto o nich decyduje.
Najczęściej zadawane pytania (FAQ)
Po co przenosić Terraform z lokalnego uruchamiania do pipeline CI/CD?
Lokalne terraform apply działa tylko przy małym zespole i niewielkiej liczbie środowisk. Gdy osób i środowisk przybywa, zaczyna brakować śladu audytowego: nie wiadomo dokładnie, kto, kiedy i z jakiego commita wykonał zmiany.
Pipeline CI/CD daje powtarzalność (ten sam kod, wersje narzędzi i kroki), centralne logi oraz spójny model uprawnień oparty o konta serwisowe. Znika konieczność nadawania pełnych uprawnień do produkcji na laptopy inżynierów.
Jak wygląda typowy pipeline CI/CD z Terraformem (validate, plan, apply)?
Najczęściej stosowana sekwencja to rozbite na joby: formatowanie/linty → walidacja → plan → apply. Na push lub merge request uruchamiane są szybkie kroki: terraform fmt -check, tflint i terraform validate (często z -backend=false dla szybkości).
Następnie pipeline robi pełne terraform init ze zdalnym backendem i terraform plan, a wynik planu trafia jako artefakt lub komentarz do MR/PR. Po akceptacji zmian merge do głównej gałęzi uruchamia job terraform apply, który korzysta bezpośrednio z wygenerowanego wcześniej pliku planu.
Czym różni się terraform plan/apply lokalnie od uruchamiania w CI/CD?
Przy lokalnym uruchamianiu zmiany zależą od środowiska na laptopie: wersji Terraforma, providerów, zmiennych i credentiali każdej osoby. Trudno odtworzyć incydent albo ustalić pełną historię modyfikacji infrastruktury.
W CI/CD te elementy są zdefiniowane w jednym miejscu (pipeline), a wszystkie wywołania plan/apply są powiązane z konkretnym commitem i merge requestem. Logi, artefakty planów i uprawnienia są centralnie zarządzane, więc łatwiej spełnić wymagania compliance i separacji obowiązków.
Jak najlepiej zorganizować katalogi pod Terraform w pipeline (monorepo vs multi-repo)?
W monorepo typowy układ to wspólne modules/ oraz katalogi środowisk, np. envs/dev, envs/stage, envs/prod. Pipeline może wtedy odpalać joby tylko dla katalogów, w których zaszły zmiany. To ułatwia kontrolę i zmniejsza czas wykonania.
W podejściu multi-repo każdy większy fragment infrastruktury (np. osobny system) ma własne repo i własny pipeline. To porządkuje odpowiedzialność między zespołami, ale utrudnia centralny governance. Niezależnie od wyboru, katalogi środowisk powinny być wyraźnie rozdzielone i mieć spójne nazwy.
Czy lepiej używać Terraform workspaces, czy osobnych katalogów dla dev/stage/prod?
Workspaces bazują na jednym zestawie plików i rozdzielnym stanie dla środowisk. Sprawdzają się, gdy infrastruktura jest prawie identyczna, środowisk jest mało, a zespół świadomie obchodzi się z przełączaniem workspace’ów (np. lokalnie). W pipeline konfiguracja jest wtedy prostsza, ale łatwiej o pomyłkę przy pracy ręcznej.
Osobne katalogi (np. envs/dev, envs/stage, envs/prod) z osobnymi backendami są czytelniejsze w większych organizacjach. Każde środowisko ma własny stan, pipeline i często osobne uprawnienia, co zmniejsza ryzyko przypadkowego apply na złym środowisku.
Dlaczego potrzebny jest zdalny backend i blokada stanu w pipeline Terraforma?
Przechowywanie terraform.tfstate lokalnie sprzyja konfliktom i nadpisywaniu zmian, zwłaszcza gdy kilka osób modyfikuje ten sam fragment infrastruktury. W CI/CD lokalny stan jest wręcz niepraktyczny, bo joby uruchamiają się na różnych runnerach.
Zdalny backend (np. S3 + DynamoDB, GCS, AzureRM, Terraform Cloud) zapewnia wspólny, trwały stan oraz mechanizm blokady (lock). Lock uniemożliwia równoczesne apply z dwóch pipeline’ów, dzięki czemu unikasz trudnych do odtworzenia błędów przy nakładających się deploymentach.
Jak podejść do automatycznego vs manualnego terraform apply na różnych środowiskach?
Częsty wzorzec to automatyczny apply niżej i ręczny wyżej. Na przykład: środowisko dev aktualizowane automatycznie po merge do develop, stage po merge do main z obowiązkowym code review, a produkcja tylko przez manualny job z zatwierdzeniem.
Taki model pozwala szybko testować zmiany na niższych środowiskach, a jednocześnie zachować mocniejszą kontrolę nad produkcją. Kluczowe jest to, by apply w produkcji korzystał dokładnie z tego planu, który został zrecenzowany i zaakceptowany w pipeline.
Bibliografia
- Terraform: Up & Running, 3rd Edition. O'Reilly Media (2022) – Praktyczne wzorce użycia Terraforma, moduły, środowiska, CI/CD
- Terraform Documentation – CLI Commands (init, validate, plan, apply). HashiCorp – Oficjalne opisy kroków init/validate/plan/apply i ich parametrów
- Continuous Delivery: Reliable Software Releases through Build, Test, and Deployment Automation. Addison-Wesley (2010) – Fundamenty CI/CD, wzorce pipeline’ów, kontrola zmian i audyt
- AWS Well-Architected Framework – Operational Excellence Pillar. Amazon Web Services – Zalecenia dot. automatyzacji, śladu audytowego i zarządzania zmianą
- Google Cloud DevOps Tech Paper – SRE and CI/CD Practices. Google Cloud – Praktyki DevOps, przegląd zmian, pipeline jako centralny punkt kontroli
- Azure DevOps – YAML Pipelines for Infrastructure as Code. Microsoft – Tworzenie pipeline’ów IaC, rozdzielenie kroków validate/plan/apply
- GitLab CI/CD Pipelines for Terraform. GitLab – Przykładowe joby Terraform plan/apply, artefakty, ochrona gałęzi
- Terraform Linting and Security Scanning with TFLint and tfsec. Aqua Security – Linting i skanowanie bezpieczeństwa Terraform w pipeline CI/CD






