Terraform w pipeline: walidacja, plan i apply z kontrolą zmian

0
58
5/5 - (1 vote)

Nawigacja:

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średniego apply,
  • 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=false i terraform validate dla szybkiego feedbacku,
  • job plan – pełne terraform init ze zdalnym backendem i terraform 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:

  1. Developer robi zmianę w modułach lub konfiguracji środowiska.
  2. Tworzy branch i otwiera merge request / pull request.
  3. Automatycznie uruchamia się lint/validate oraz terraform plan dla danego środowiska lub zestawu środowisk.
  4. Plan jest publikowany jako artefakt lub komentarz do MR/PR.
  5. Zespół przegląda kod i wynik planu. Jeśli wszystko jest akceptowalne – merge.
  6. 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.

PlatformaTypowa integracja TerraformaCechy istotne dla kontroli zmian
GitHub ActionsWorkflow YAML w repo, akcje społecznościEnvironment protection, approvals, komentarze do PR
GitLab CI.gitlab-ci.yml, artifacts, environmentsManual jobs, rules, review apps, protected branches
Azure DevOpsPipelines YAML, Service ConnectionsApprovals & checks, variable groups, RBAC
JenkinsJenkinsfile, pluginy, własna orkiestracjaDuż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 validate

Wyłą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:

  1. terraform plan -out=plan.tfplan
  2. terraform show -json plan.tfplan > plan.json
  3. conftest test plan.json z 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.tfplan jako artefakt do późniejszego apply.

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.

Panel sterowania przemysłową automatyką w nowoczesnej hali
Źródło: Pexels | Autor: Ludovic Delot

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.tfplan jako 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 plan na 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 plan działa automatycznie po merge do main,
  • job apply jest 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 apply z 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-HHMM wyzwala 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.txt

Osobny 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