janol.pl_

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.

Dlaczego nie gołe S3 + DynamoDB lock Backend S3 z blokadą przez DynamoDB też działa i jest tańszy w utrzymaniu, jeśli już siedzisz w AWS. Ale nie daje UI do przeglądu planu przed apply, nie kolejkuje runów w sposób widoczny dla całego zespołu i nie ma wbudowanego RBAC per workspace. Terrakube kupuje wszystko to za cenę jednej dodatkowej usługi do utrzymania.

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.

← wszystkie wpisy