Praktyka
Terraform + Terrakube w praktyce: IaC bez apply z laptopa
Terraform sam w sobie rozwiązuje jeden problem — deklaratywny opis infrastruktury. Nie rozwiązuje drugiego, ważniejszego w zespole: kto, kiedy i z jakim stanem odpalił apply. Przez pierwsze miesiące u obecnego pracodawcy stan leżał lokalnie, a terraform apply ktoś odpalał z laptopa, licząc na to, że nikt inny w tym samym czasie nie robi tego samego. Działało, dopóki nie przestało — dwie osoby, dwa równoległe plany, jeden zasób nadpisany bez ostrzeżenia. Terrakube to było rozwiązanie tego konkretnego bólu, nie moda na kolejne narzędzie.
Terrakube to open-source'owy, self-hosted odpowiednik Terraform Cloud — ma workspace'y, zdalny stan, kolejkowanie runów, zmienne per-workspace i UI do przeglądania planów przed zatwierdzeniem. W przeciwieństwie do Terraform Cloud stoi na własnej infrastrukturze, więc nie płacimy per-seat i nie wysyłamy stanu poza sieć firmy — istotne, gdy w stanie leżą adresy wewnętrznych usług, ID kont AD czy inne rzeczy, których nie chcemy w chmurze zewnętrznego dostawcy.
Struktura repozytorium i modułów
Moduły trzymam osobno od konfiguracji środowisk. Repo terraform-modules zawiera wielokrotnego użytku bloki (sieć, LDAP-owy VM, para HAProxy), a osobne repo per środowisko odwołuje się do nich przez tag wersji, nigdy przez main:
terraform-modules/
├── modules/
│ ├── vm-linux/
│ ├── network-vlan/
│ └── haproxy-pair/
└── CHANGELOG.md
infra-prod/
├── environments/
│ ├── prod/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── backend.tf
│ └── staging/
└── .gitlab-ci.yml
Pinowanie wersji modułu to nie formalność — to jedyny sposób, żeby zmiana w module nie wjechała cicho na produkcję przy okazji niepowiązanego PR-a gdzie indziej:
module "haproxy_prod" {
source = "git::https://gitlab.internal/infra/terraform-modules.git//modules/haproxy-pair?ref=v2.4.0"
env = "prod"
vip = var.haproxy_vip
}
Remote state i workspace'y
Backend wskazuje na Terrakube, a nie na plik lokalny czy S3-owy bucket, który każdy administrator konfiguruje trochę inaczej:
terraform {
cloud {
hostname = "terrakube.internal.example.com"
organization = "atende"
workspaces {
name = "infra-prod"
}
}
}
Każde środowisko to osobny workspace z własnym zestawem zmiennych i własną historią runów. Blokada stanu jest wbudowana — kiedy ktoś odpala plan na tym samym workspace, drugi run czeka w kolejce zamiast walczyć o ten sam .tfstate. To dokładnie ten problem, który wywalał mi zasoby na starcie.
Integracja z GitLab CI
Pipeline nie odpala terraform apply bezpośrednio — woła API Terrakube, które samo zarządza runem. To ważne rozróżnienie: GitLab jest tu triggerem i miejscem code review, a nie wykonawcą. Wykonawcą jest zawsze Terrakube, z tym samym stanem, tymi samymi zmiennymi i tą samą historią niezależnie od tego, kto i skąd odpalił zmianę.
stages:
- plan
- apply
terraform_plan:
stage: plan
image: curlimages/curl:8.9
script:
- >
curl -sf -X POST
-H "Authorization: Bearer ${TERRAKUBE_TOKEN}"
https://terrakube.internal.example.com/api/v1/organizations/atende/workspaces/infra-prod/runs
-d '{"type":"plan"}'
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
terraform_apply:
stage: apply
image: curlimages/curl:8.9
script:
- >
curl -sf -X POST
-H "Authorization: Bearer ${TERRAKUBE_TOKEN}"
https://terrakube.internal.example.com/api/v1/organizations/atende/workspaces/infra-prod/runs
-d '{"type":"apply"}'
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Plan leci automatycznie na każdy merge request — recenzent widzi w komentarzu MR, co dokładnie się zmieni, zanim ktokolwiek to zatwierdzi. Apply na produkcji jest zawsze when: manual. To świadoma decyzja: automatyczny plan buduje zaufanie do narzędzia, ale ostatni krok na produkcyjnej infrastrukturze wciąż wymaga świadomego kliknięcia, nie ślepej automatyzacji CI.
Dlaczego to, a nie apply z laptopa
Trzy konkretne sytuacje, które przesądziły sprawę:
- Wspólny, zablokowany stan — koniec z „czy ktoś teraz robi plan na prod?” na czacie zespołowym.
- Ślad audytowy — każdy apply ma powiązany commit, autora i log runu, do wglądu miesiące później, kiedy trzeba ustalić, kto zmienił regułę firewalla.
- Spójne środowisko wykonania — Terrakube zawsze uruchamia tę samą wersję Terraform i tych samych providerów, zdefiniowaną w konfiguracji workspace. Laptop administratora ma tę wersję, którą akurat ma zainstalowaną — a to prosta droga do „u mnie plan wygląda inaczej”.
Terraform Cloud kosztuje per seat, Terrakube kosztuje jedną maszynę wirtualną i trochę czasu na konfigurację. Dla zespołu, który już ma gdzie hostować, to prosty rachunek.
Pułapka, w którą wpadłem
Pierwszy poważny problem to nie był Terraform, tylko kolejność apply między modułami zależnymi od siebie w różnych workspace'ach — sieć musi powstać przed VM, VM przed wpisem w HAProxy. Terrakube nie zna zależności między workspace'ami automatycznie, trzeba je modelować ręcznie przez data sources czytające output z innego stanu albo — prościej — pilnować kolejności w pipeline. Rozwiązałem to, trzymając moduły sieciowe i kompute w jednym workspace tam, gdzie zależność jest ścisła, i rozdzielając tylko tam, gdzie cykl życia komponentów faktycznie się różni.
Ten sam nawyk „nic ważnego nie wykonuje się lokalnie” prowadzi mnie zresztą w Dockerze — pisałem o tym przy okazji tego, dlaczego nie buduję obrazów na serwerze. Zasada jest ta sama: jeśli coś ma trafić na produkcję, przechodzi przez wspólny, kontrolowany pipeline, a nie przez maszynę jednej osoby.