WebDisk
Chmura publiczna

Zarządzanie Apache CloudStack za pomocą Terraform: od kluczy API po klaster Kubernetes

Data publikacji:

Blog WebDisk · kategoria: Chmura publiczna · czas czytania: ~26 minut

W skrócie:- Apache CloudStack to open-source'owy orkiestrator chmury IaaS — jedno API nad serwerami KVM, storage'em i siecią. Jego pojęcia (strefa, oferta, sieć, VPC, projekt) przekładają się niemal 1:1 na zasoby Terraforma.- Konfiguracja providera to trzy wartości: adres API, klucz i sekret. Reszta to zwykły Terraform — plan, apply, stan trzymany zdalnie w S3, bez żadnej dodatkowej usługi do blokad.- Pokazujemy pełny przykład: klaster Kubernetes na CloudStacku opisany kodem — od VPC i reguł ACL po gotowy kubeconfig i komponenty wdrożone Helmem. >Nie pracujesz z terminalem? Pomiń bloki kodu — pierwsza połowa tekstu opisuje sam CloudStack i jego architekturę, i czyta się bez nich.

Jest poniedziałek, wpada zgłoszenie: „potrzebujemy takiego samego środowiska jak produkcyjne, tylko mniejszego”. W chmurze klikanej ręcznie ta prośba oznacza dwa dni pracy, listę w notatniku i cichą pewność, że coś się jednak będzie różnić – bo ktoś zapomni o jednej regule firewalla albo wybierze inną ofertę dyskową. W chmurze opisanej kodem oznacza skopiowanie katalogu, zmianę czterech liczb w pliku ze zmiennymi i uruchomienie pipeline'u.

Ten artykuł jest o tym, jak dojść do tego drugiego stanu na Apache CloudStack – platformie, na której działa nasza chmura publiczna. Zaczniemy od solidnego opisu samego CloudStacka: czym jest, z czego się składa i jakimi pojęciami operuje – bo bez tego Terraform jest tylko składnią. Potem przejdziemy do konfiguracji providera, pierwszych zasobów i pełnego, realnego przykładu: klastra Kubernetes, który stawiamy u siebie w całości z Terraforma. Na końcu – lista pułapek, które kosztowały nas czas, żebyś Ty nie musiał ich odkrywać samodzielnie. Tekst jest dla dwóch grup: administratorów, którzy dopiero poznają CloudStacka i chcą zrozumieć jego pojęcia, oraz zespołów znających Terraforma z hiperskalerów i szukających odpowiedników w chmurze open source.

Czym właściwie jest Apache CloudStack?

Apache CloudStack to open-source'owa platforma do budowy chmur IaaS – oprogramowanie, które zamienia szafy pełne serwerów, macierzy i przełączników w chmurę z samoobsługowym panelem i API. Użytkownik klika „utwórz maszynę”, a CloudStack decyduje, na którym fizycznym hoście ją uruchomić, tworzy jej dysk w storage'u, wpina ją do właściwej sieci, przydziela adres IP i zwraca konsolę w przeglądarce. To warstwa, która w hiperskalerach nazywa się „control plane” – tylko że tutaj możesz ją pobrać, przeczytać i uruchomić u siebie.

Projekt ma długą historię: powstał jako komercyjny produkt firmy Cloud.com, trafił do Citriksa, a w 2012 roku został przekazany Apache Software Foundation, gdzie w 2013 roku stał się projektem najwyższego poziomu. Od tego czasu rozwija się jako klasyczny projekt ASF – z publiczną listą dyskusyjną, głosowaniami nad wydaniami i licencją Apache 2.0, bez „wersji enterprise” i bez pułapki podwójnego licencjonowania. Napędza chmury publiczne, chmury telekomów i prywatne środowiska w firmach, które nie chcą oddawać kontroli nad warstwą wirtualizacji.

Warto od razu ustawić CloudStacka w kontekście, bo najczęstsze pytanie brzmi: „a czym to się różni od OpenStacka?”. Najkrócej: filozofią złożoności. OpenStack jest zbiorem kilkudziesięciu współpracujących projektów (Nova, Neutron, Cinder, Keystone, Glance…), z których każdy ma własną bazę, własne API i własny cykl życia – daje to ogromną elastyczność kosztem sporego zespołu utrzymującego samą platformę. CloudStack to jedna aplikacja Javy (management server) i jedna baza MySQL, w której siedzi cały model chmury. Instalacja mieści się w kilku pakietach, aktualizacja to podmiana pakietu i migracja schematu bazy. Za tę prostotę płaci się mniejszą modułowością – trudniej wymienić pojedynczy podsystem na własny. Dla operatora średniej wielkości to zwykle dobry kurs wymiany.

CloudStack sam nie wirtualizuje – steruje hypervisorami (hypervisor to oprogramowanie, które uruchamia maszyny wirtualne na fizycznym serwerze). Obsługuje KVM (najpopularniejszy wybór i nasz), VMware vSphere, XCP-ng/XenServer oraz kilka mniej dziś istotnych. Dla storage'u mówi językiem NFS, iSCSI, lokalnych dysków i – co dla nas kluczowe – Ceph RBD.

Hierarchia infrastruktury: strefa, pod, klaster, host

Żeby rozmawiać z CloudStackiem, trzeba przyjąć jego sposób dzielenia świata. Warstwy idą od największej do najmniejszej:

  • Strefa (zone) – największa jednostka, w praktyce jedno centrum danych albo jedna strefa dostępności. Strefa ma własną sieć publiczną, własny secondary storage i własny zestaw maszyn systemowych. To pierwsza rzecz, którą podajesz przy tworzeniu czegokolwiek.
  • Pod – zwykle jedna szafa lub rząd szaf, w praktyce jedna domena rozgłoszeniowa dla sieci zarządzającej.
  • Klaster (cluster) – grupa hostów tego samego typu hypervisora, dzielących ten sam primary storage. To granica, w której CloudStack może swobodnie migrować maszyny na żywo.
  • Host – pojedynczy fizyczny serwer z hypervisorem i agentem CloudStacka.

Do tego dwa rodzaje przestrzeni dyskowej, których nie należy mylić:

  • Primary storage – tu żyją dyski działających maszyn. Musi być szybki i widoczny dla hostów w klastrze. U nas to pule Ceph RBD (osobna na NVMe, osobna na dyski pojemnościowe).
  • Secondary storage – magazyn szablonów systemów, obrazów ISO, snapshotów i wolumenów eksportowanych. Nie musi być szybki, musi być pojemny i dostępny w całej strefie. Klasycznie NFS.

Maszyny systemowe, czyli chmura, która obsługuje sama siebie

Jedna z bardziej charakterystycznych cech CloudStacka: część własnej funkcjonalności realizuje maszynami wirtualnymi, które sam sobie tworzy. Nie ma tu wielkiego, scentralizowanego routera programowego ani osobnego klastra usług sieciowych – jest garść małych VM-ek, po jednej od zadania:

  • Virtual Router (r-*-VM) – serce sieci tenanta, czyli pojedynczego klienta lub projektu odizolowanego od pozostałych. Dla każdej sieci izolowanej lub VPC CloudStack uruchamia router, który świadczy DHCP, DNS, SNAT (podmianę adresu źródłowego, dzięki której maszyny z sieci prywatnej wychodzą do internetu wspólnym adresem publicznym), przekierowania portów, firewall, load balancer i VPN site-to-site. Można go zamówić w wariancie redundantnym: para routerów dzieli jeden adres protokołem VRRP, więc gdy aktywny padnie, drugi przejmuje go w kilka sekund.
  • Secondary Storage VM (s-*-VM) – obsługuje pobieranie i rejestrowanie szablonów, wgrywanie ISO, snapshoty i kopiowanie obrazów między strefami.
  • Console Proxy (v-*-VM) – tuneluje VNC z hostów do przeglądarki, czyli to dzięki niej działa przycisk „Konsola” w panelu.

Taka architektura ma dwie ważne konsekwencje praktyczne. Po pierwsze: usługi sieciowe skalują się razem z liczbą sieci, bo każda dostaje własny router – nie ma jednego wąskiego gardła dla całej chmury. Po drugie, i to jest uczciwe ostrzeżenie: load balancer w CloudStacku żyje wewnątrz Virtual Routera i jest balanserem warstwy czwartej (TCP, z tzw. sesjami lepkimi, czyli przypisaniem klienta do jednego serwera na czas sesji, oraz – od CloudStacka 4.22 – z terminacją SSL na samym routerze; we wcześniejszych wydaniach odciążenie SSL wymagało zewnętrznego urządzenia). To nie jest odpowiednik ALB z routingiem po ścieżkach i nagłówkach – prawdziwy ingress warstwy siódmej trzeba postawić samemu, np. w Kubernetesie. Przepustowość takiego LB ogranicza pojedynczy router, więc przy dużym ruchu planuje się to inaczej.

Hierarchia rachunkowa: domena, konto, użytkownik, projekt

Równolegle do infrastruktury CloudStack prowadzi drugą hierarchię – tę, która decyduje, kto co widzi i za co płaci:

  • Domena – drzewiasta struktura organizacyjna. Domena ROOT zawiera wszystko, pod nią mogą wisieć domeny partnerów, a pod nimi domeny klientów, każda z własnymi limitami.
  • Konto (account) – właściciel zasobów i granica rozliczeń. Konto może mieć kilku użytkowników.
  • Użytkownik (user) – konkretna osoba lub integracja; to na poziomie użytkownika generuje się klucze API.
  • Projekt (project) – kontener na zasoby współdzielone przez wiele kont, ze wspólnym limitem i wspólnym rachunkiem.

Projekty są dla Terraforma szczególnie istotne, bo prawie każdy zasób przyjmuje parametr project – i pominięcie go oznacza, że maszyna wyląduje w prywatnej przestrzeni konta zamiast we wspólnej. To jeden z częstszych powodów „dlaczego nie widzę tego, co przed chwilą stworzyłem”.

Oferty (offerings): menu, którego nie układa użytkownik

Tu CloudStack wyraźnie różni się od AWS-a i to jest różnica, o którą potyka się każdy, kto przychodzi z hiperskalerów. W CloudStacku nie definiujesz w Terraformie parametrów maszyny – wybierasz pozycję z menu, które ułożył operator chmury. To menu to oferty:

  • Service offering – profil obliczeniowy: liczba vCPU, częstotliwość, RAM, ewentualne limity IOPS (liczby operacji dyskowych na sekundę) i tagi hosta. Może być sztywna (jak t3.medium) albo customized – wtedy sam podajesz parametry przy tworzeniu maszyny, w granicach ustalonych przez operatora.
  • Disk offering – profil wolumenu: rozmiar (lub „custom”), typ storage'u, gwarancje IOPS/przepustowości.
  • Network offering – najciekawsza z nich: definiuje, jakie usługi świadczy sieć. Czy ma własny router z SNAT, czy ma DHCP i DNS, czy udostępnia load balancer, firewall, VPN, czy pracuje w trybie redundantnym. Zmiana oferty sieciowej zmienia możliwości sieci, nie tylko jej „rozmiar”.
  • VPC offering – to samo piętro wyżej, dla całej chmury prywatnej.

Praktyczny wniosek dla Terraforma: w kodzie odwołujesz się do ofert po nazwie albo po UUID, a nie opisujesz ich parametrów. Jeśli oferta jest typu „customized”, konkretne wartości wędrują w pole details.

Sieć: izolowana, współdzielona i VPC

Model sieciowy CloudStacka wygląda tak:

  • Sieć izolowana (isolated) – prywatna sieć jednego konta lub projektu, z własnym VLAN-em lub VXLAN-em (to dwa sposoby wydzielenia osobnej sieci na wspólnym okablowaniu; VXLAN jest nowszy i znosi właściwy VLAN-om limit około czterech tysięcy sieci) i własnym Virtual Routerem. Router robi SNAT do internetu, a ruch przychodzący przepuszczasz przez przekierowania portów, static NAT albo reguły LB. Firewall działa na publicznym adresie, jest stanowy i domyślnie zamknięty.
  • Sieć współdzielona (shared) – wspólna sieć dla wielu kont, zwykle z pulą adresów przydzielanych przez DHCP. Bez własnego routera per tenant.
  • VPC – chmura prywatna z wieloma tierami (podsieciami), wspólnym routerem i listami ACL między tierami. Odpowiednik VPC z AWS-a, z jedną istotną różnicą: reguły ACL w CloudStacku są stanowe, więc odpowiedź na dozwolone połączenie wraca bez osobnej reguły; ingress i egress są numerowane i konfigurowane oddzielnie. To wariant, którego używamy do wszystkiego, co poważniejsze.
  • Grupy bezpieczeństwa (security groups) – filtr ruchu przypinany do samych maszyn, a nie do sieci. Działają w strefach typu „basic”, a także w strefach „advanced”, o ile operator włączył je przy zakładaniu strefy – izolują wtedy maszyny w jednej sieci współdzielonej na całą strefę (na KVM; na XenServerze/XCP-ng tylko przy sieci w trybie mostka). Nie łączy się ich z sieciami izolowanymi ani z VPC – tam rolę filtra pełnią firewall na publicznym adresie i listy ACL.

API: jedno, podpisywane, asynchroniczne

Cała chmura wystawia jedno API HTTP pod adresem /client/api. Wywołania są podpisywane algorytmem HMAC-SHA1 parą klucz API + sekret, a odpowiedź dostajesz w JSON-ie lub XML-u. Panel webowy nie ma żadnych „prywatnych” endpointów – klika dokładnie to samo API, co Ty.

Druga cecha, którą trzeba znać, zanim zacznie się automatyzować: większość operacji zmieniających stan jest asynchroniczna. deployVirtualMachine nie zwraca gotowej maszyny – zwraca identyfikator zadania, którego postęp odpytujesz przez queryAsyncJobResult. Provider Terraforma robi to za Ciebie, ale to wyjaśnia, dlaczego niektóre operacje trwają, dlaczego pojawia się „eventual consistency” i dlaczego czasem trzeba jawnie poczekać na gotowość usługi (wrócimy do tego przy przykładzie z Kubernetesem).

Do ręcznej pracy z tym API służy oficjalny klient CloudMonkey (cmk) – świetny do eksploracji i do wyciągania UUID-ów, które potem wkleisz do Terraforma:

# lista stref i ofert obliczeniowych — przydatne przy pisaniu pierwszego modułu
cmk list zones filter=id,name
cmk list serviceofferings filter=id,name,cpunumber,memory
cmk list networkofferings filter=id,name,state

Dlaczego opisywać CloudStacka kodem, a nie klikać w panelu?

Panel CloudStacka jest kompletny – da się w nim zrobić wszystko. Problem nie leży w możliwościach, tylko w tym, czego panel z natury nie zapewnia:

  • Powtarzalność. Środowisko testowe „takie samo jak produkcja” zbudowane ręcznie nigdy nie jest takie samo. Zbudowane z tego samego modułu z innym plikiem zmiennych – jest.
  • Przegląd zmian przed wykonaniem. terraform plan pokazuje różnicę między stanem opisanym a rzeczywistym, zanim cokolwiek się wydarzy. W panelu odpowiednikiem jest ostrożność operatora.
  • Historia i odpowiedzialność. Kto dodał regułę ACL i dlaczego? W repozytorium to jeden git blame. W panelu – w najlepszym razie wpis w logu zdarzeń bez uzasadnienia.
  • Wykrywanie dryfu. Ktoś zmienił coś ręcznie „na chwilę”? Najbliższy plan to pokaże.
  • Odtworzenie po katastrofie. Kod to najbardziej wiarygodna dokumentacja architektury – taka, która na pewno jest aktualna, bo z niej powstało środowisko.

Cena jest realna i warto ją znać: kod trzeba utrzymywać, stan trzeba chronić, a pierwsze wdrożenie zajmie więcej czasu niż wyklikanie. Zwrot przychodzi przy drugim środowisku i przy pierwszej awarii.

Jak skonfigurować Terraform z CloudStackiem?

Konfiguracja to cztery kroki: klucze API, provider, świadomy wybór jego wydania i przeniesienie stanu na zdalny backend. Trzy z nich robi się raz na projekt i można o nich zapomnieć. Czwarty – wybór providera – to decyzja, do której najczęściej się wraca, więc poświęcimy jej najwięcej miejsca.

Krok 1: klucze API

Terraform uwierzytelnia się kluczami użytkownika CloudStacka. Generujesz je w panelu: Accounts → wybierz konto → Users → wybierz użytkownika → Generate Keys, po czym kopiujesz API Key i Secret Key. Sekret zobaczysz w całości tylko przy generowaniu – potem panel go maskuje.

Dwie zasady, których warto trzymać się od pierwszego dnia:

  • Osobny użytkownik dla automatyzacji. Nie używaj kluczy człowieka. Konto techniczne łatwiej odebrać, zrotować i wyśledzić w logu zdarzeń.
  • Najmniejsze potrzebne uprawnienia. Jeśli Terraform ma zarządzać jednym projektem, nie dawaj mu roli administratora domeny.

Krok 2: provider i zmienne

Konfiguracja providera to dosłownie trzy wartości. Kluczowe jest to, żeby dwie z nich nigdy nie trafiły do repozytorium:

# providers.tf
terraform {
  required_version = "~> 1.10"

  required_providers {
    cloudstack = {
      source = "cloudstack/cloudstack"
      version = "0.6.0"
    }
  }
}

provider "cloudstack" {
  api_url = var.cloudstack_api_url
  api_key = var.cloudstack_api_key
  secret_key = var.cloudstack_secret_key
}

# variables.tf
variable "cloudstack_api_url" {
  description = "Adres API CloudStacka, np. https://panel.example.com/client/api"
  type = string
}

variable "cloudstack_api_key" {
  description = "Klucz API użytkownika technicznego"
  type = string
  sensitive = true # nie pokaże się w logach plan/apply
}

variable "cloudstack_secret_key" {
  description = "Sekret użytkownika technicznego"
  type = string
  sensitive = true
}

Wartości podajesz zmiennymi środowiskowymi – Terraform czyta każdą zmienną o prefiksie TF_VAR_:

export TF_VAR_cloudstack_api_url="https://panel.example.com/client/api"
export TF_VAR_cloudstack_api_key="$(pass show cloud/terraform/api-key)"
export TF_VAR_cloudstack_secret_key="$(pass show cloud/terraform/secret-key)"

terraform init
terraform plan

W pipelinie te same trzy wartości wstawiasz jako zamaskowane zmienne CI/CD. Plik *.tfvars z sekretami to najczęstszy sposób, w jaki klucze trafiają przypadkiem do gita – lepiej po prostu go nie tworzyć.

Krok 3: wersja providera ma znaczenie

To miejsce, w którym dokumentacja bywa optymistyczna, więc powiemy wprost, jak jest. Oficjalny provider cloudstack/cloudstack rozwija się w repozytorium Apache i jest właściwym punktem startu – tyle że w rytmie, do którego trzeba się przyzwyczaić: wydania stabilne pojawiają się co półtora–dwa lata (0.4.0 – luty 2022, 0.5.0 – kwiecień 2024, 0.6.0 – październik 2025). Ostatnie stabilne wydanie w rejestrze to 0.6.0 z 27 października 2025 roku, a gałąź 0.7.0 jest tam na razie wyłącznie kandydatem do wydania (0.7.0-rc1 z 19 sierpnia 2026, opublikowany tuż przed powstaniem tego tekstu; stan na sierpień 2026). Przez taką przerwę w gałęzi głównej repozytorium zbiera się zapas zasobów i atrybutów, których stabilne wydanie jeszcze nie ma. Praktyczny wniosek: zanim uznasz, że providerowi czegoś brakuje, zajrzyj do CHANGELOG-u – funkcja, której nie było rok temu, zdążyła już wejść do wydania stabilnego.

Dobrą ilustracją jest atrybut cidrlist w zasobie cloudstack_loadbalancer_rule – możliwość ograniczenia listą adresów, kto w ogóle może dobić się do reguły load balancera. W wydaniu 0.5.0 jeszcze go nie było; do gałęzi głównej wszedł pod koniec sierpnia 2025 i jest w stabilnym 0.6.0. Morał jest prosty i powtarzalny: brak, który obchodziłeś rok temu, dziś bywa już domknięty w wydaniu stabilnym – warto sprawdzić, zanim zbudujesz obejście, które trzeba będzie utrzymywać.

Praktyczne zalecenia:

  • Startuj od oficjalnego cloudstack/cloudstack z przestrzeni Apache – to on jest referencją dla dokumentacji i przykładów, także tych w tym artykule.
  • Provider to zależność o najwyższych uprawnieniach. Jest programem, który uruchamiasz na swoim laptopie i na runnerze CI: z kluczami API do całej chmury, z wartościami atrybutów oznaczonych jako sensitive i w tym samym środowisku, w którym leżą poświadczenia do bucketa ze stanem. Traktuj go jak każdą inną zależność o takim zasięgu: bierz z oficjalnej przestrzeni w rejestrze, a jeśli polityka firmy tego wymaga – buduj sam ze sprawdzonego źródła i podawaj z lokalnego lustra (filesystem_mirror). Miej przy tym świadomość granicy: .terraform.lock.hcl chroni przed cichą podmianą binarki po fakcie, nie przed kodem, który był zły od początku.
  • Pinuj dokładną wersję (version = "0.6.0", nie >= 0.6) i commituj plik .terraform.lock.hcl. Chmura, która się „sama zmieniła”, bo pipeline pobrał nowszego providera, to bardzo nieprzyjemna klasa awarii.
  • OpenTofu działa tak samo. W 2023 roku Terraform przeszedł z licencji open source na BUSL (Business Source License): kod nadal jest jawny, a użytek wewnętrzny – także komercyjny – darmowy, ale formalnie nie jest to już open source, bo nie wolno oferować Terraforma jako usługi konkurencyjnej wobec płatnych produktów HashiCorpa. W odpowiedzi powstał fork pod Linux Foundation; providery CloudStacka są zwykłymi pluginami i obsługuje je jedno i drugie narzędzie. Jeśli licencja BUSL jest dla Ciebie problemem – najczęściej dlatego, że polityka firmy dopuszcza wyłącznie licencje open source – zamiana terraform na tofu w poleceniach to zwykle cała migracja.

Krok 4: stan (state) – i miła niespodzianka dla operatorów CloudStacka

Terraform trzyma w pliku stanu odwzorowanie tego, co utworzył. Trzymanie go lokalnie działa dokładnie do momentu, w którym drugi człowiek albo pipeline zrobią apply – potem zaczyna się dramat. Stan idzie więc do backendu zdalnego, a tu operator CloudStacka jest w komfortowej sytuacji, bo prawie zawsze ma pod ręką storage zgodny z S3.

Do niedawna backend s3 wymagał do blokowania stanu tabeli DynamoDB – czyli usługi, której poza AWS-em po prostu nie ma. Od Terraforma 1.10 istnieje natywna blokada oparta na warunkowych zapisach w S3 (use_lockfile = true), a w 1.11 wariant z DynamoDB został oznaczony jako przestarzały. Dla każdego, kto stoi na Ceph RGW, MinIO czy innym S3, oznacza to, że pełnoprawny backend ze współdzieloną blokadą to dziś kilkanaście linijek konfiguracji:

# backend.tf
terraform {
  backend "s3" {
    bucket = "terraform-states"
    key = "cloudstack/prod.tfstate"

    endpoints = {
      s3 = "https://s3.example.com"
    }

    region = "us-east-1" # wymagane formalnie; dla S3 spoza AWS bez znaczenia

    # wyłączamy walidacje właściwe wyłącznie dla AWS-a
    skip_credentials_validation = true
    skip_region_validation = true
    skip_requesting_account_id = true
    skip_s3_checksum = true

    use_lockfile = true # blokada natywna w S3, bez DynamoDB (Terraform >= 1.10)
  }
}

Klucze do bucketa podajesz przy inicjalizacji, żeby nie zapisywać ich w repozytorium:

terraform init \
  -backend-config="access_key=$S3_ACCESS_KEY" \
  -backend-config="secret_key=$S3_SECRET_KEY"

Jeśli korzystasz z Object Storage w WebDisk, masz backend Terraforma gotowy – to ten sam bucket i te same klucze, których używasz do wszystkiego innego.

Uwaga – o tym łatwo zapomnieć: plik stanu zawiera wartości atrybutów jawnie, łącznie z tymi oznaczonymi jako sensitive. Bucket ze stanami traktuj jak magazyn sekretów: prywatny, z szyfrowaniem po stronie serwera, z wersjonowaniem i z dostępem ograniczonym do tych, którzy naprawdę go potrzebują.

Pierwszy zasób: maszyna wirtualna

Najkrótsza sensowna konfiguracja wygląda tak. Zwróć uwagę, że parametry maszyny nie są wymyślane w kodzie – są wskazaniem na ofertę:

data "cloudstack_zone" "main" {
  filter {
    name = "name"
    value = "^PL-DCO-1$"
  }
}

data "cloudstack_template" "ubuntu" {
  template_filter = "featured"
  filter {
    name = "name"
    value = "^Ubuntu 24\\.04 LTS$"
  }
}

resource "cloudstack_network" "app" {
  name = "app-net"
  cidr = "10.20.0.0/24"
  network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService"
  zone = data.cloudstack_zone.main.id
}

resource "cloudstack_instance" "web" {
  name = "web-01"
  display_name = "web-01"
  service_offering = "Medium Instance" # pozycja z menu operatora
  template = data.cloudstack_template.ubuntu.id
  network_id = cloudstack_network.app.id
  zone = data.cloudstack_zone.main.id
  root_disk_size = 40

  # cloud-init: konfiguracja systemu przy pierwszym starcie
  user_data = base64encode(file("${path.module}/cloud-init/web.yaml"))

  expunge = true # destroy naprawdę usuwa, nie zostawia w koszu
}

Kilka rzeczy, które warto skomentować:

  • Wartości w blokach filter to wyrażenia regularne, nie nazwy. Provider kompiluje je jako regex i dopasowuje bez zakotwiczenia, więc Ubuntu 24.04 LTS złapie także Ubuntu 24.04 LTS minimal – a przy kilku trafieniach data source szablonu wybierze ten o najnowszej dacie utworzenia, niekoniecznie ten, o który Ci chodziło (data source strefy – po prostu ostatni z listy). Stąd \\. i ^…$ wyżej; przy bardziej zaśmieconej bibliotece szablonów warto dołożyć drugi filtr, np. { name = "hypervisor", value = "KVM" }.
  • service_offering po nazwie. Działa i jest czytelne, ale nazwa oferty może się zmienić. W środowiskach, które mają żyć długo, bezpieczniej wskazać UUID.
  • Oferta typu „customized” nie ma zaszytych parametrów – podaje się je w polu details, np. details = { cpuNumber = "4", cpuSpeed = "2000", memory = "8192" }. Bez tego CloudStack odrzuci żądanie.
  • user_data to standardowy kanał cloud-init. Uwaga na limit rozmiaru – przy większych konfiguracjach pomaga base64gzip() zamiast base64encode().
  • expunge = true oznacza, że terraform destroy naprawdę kasuje maszynę, zamiast zostawiać ją w stanie „Destroyed” do czasu wygaśnięcia. Dla środowisk efemerycznych to właściwy wybór; dla produkcji przemyśl.

VPC i tiery: reguły ACL nie są osobnymi zasobami

Poważniejsze środowiska buduje się na VPC. Wygląda to naturalnie: chmura prywatna, w niej sieci, a przed sieciami – lista ACL:

resource "cloudstack_vpc" "main" {
  name = "prod-vpc"
  cidr = "10.0.0.0/16"
  vpc_offering = "Redundant VPC offering"
  zone = data.cloudstack_zone.main.id
  project = var.project
}

resource "cloudstack_network_acl" "app" {
  name = "prod-acl"
  description = "ACL dla tieru aplikacyjnego"
  vpc_id = cloudstack_vpc.main.id
  project = var.project
}

resource "cloudstack_network_acl_rule" "app" {
  acl_id = cloudstack_network_acl.app.id
  project = var.project

  # UWAGA: wszystkie reguły jednej ACL mieszkają w JEDNYM zasobie
  rule {
    action = "allow"
    cidr_list = ["0.0.0.0/0"]
    protocol = "all"
    traffic_type = "egress"
  }

  # jeden port albo jeden zakres na blok `rule` — 0.6.0 nie przyjmuje już listy
  rule {
    action = "allow"
    cidr_list = var.admin_cidrs # sieci administracyjne / VPN
    protocol = "tcp"
    port = "22"
    traffic_type = "ingress"
  }

  rule {
    action = "allow"
    cidr_list = var.admin_cidrs
    protocol = "tcp"
    port = "6443"
    traffic_type = "ingress"
  }

  rule {
    action = "allow"
    cidr_list = ["0.0.0.0/0"]
    protocol = "tcp"
    port = "443"
    traffic_type = "ingress"
  }
}

resource "cloudstack_network" "app" {
  name = "prod-app-tier"
  cidr = "10.0.1.0/24"
  network_offering = "Isolated Network for VPC with SNAT"
  vpc_id = cloudstack_vpc.main.id
  acl_id = cloudstack_network_acl.app.id
  zone = data.cloudstack_zone.main.id
  project = var.project
}

To jest miejsce na pierwszą prawdziwą pułapkę. Zasób cloudstack_network_acl_rule nie reprezentuje pojedynczej reguły – reprezentuje cały zestaw reguł danej ACL. Bloki rule wewnątrz niego to pełna lista. Jeżeli ktoś dołoży regułę ręcznie w panelu, najbliższy apply ją usunie, bo Terraform doprowadzi zestaw do stanu opisanego w kodzie. To zachowanie prawidłowe i pożądane – ale zaskakuje, gdy człowiek spodziewa się modelu „jeden zasób, jedna reguła”.

I od razu druga rzecz, która potrafi zaskoczyć: pole ports zniknęło. W 0.6.0 jest oznaczone jako przestarzałe, a provider wprost odmawia utworzenia nowej reguły, która go używa – dostaniesz błąd „The 'ports' field is no longer supported for creating new rules”. Zamiast listy podajesz port z jednym portem albo jednym zakresem ("80" lub "80-90"), więc dwa porty to po prostu dwa bloki rule. W drugą stronę działa to równie bezlitośnie: w gałęzi 0.5.x pola port jeszcze nie ma i obowiązuje wyłącznie ports. To dobry przykład tego, dlaczego warto pinować dokładną wersję i trzymać .terraform.lock.hcl w repozytorium.

Publiczny adres: najpierw rezerwacja, potem NAT i load balancer

Kolejność jest zawsze ta sama:

resource "cloudstack_ipaddress" "public" {
  vpc_id = cloudstack_vpc.main.id
  zone = data.cloudstack_zone.main.id
  project = var.project
}

# wariant 1: proste przekierowanie portu na jedną maszynę
resource "cloudstack_port_forward" "ssh" {
  ip_address_id = cloudstack_ipaddress.public.id
  project = var.project

  forward {
    protocol = "tcp"
    private_port = 22
    public_port = 2222
    virtual_machine_id = cloudstack_instance.bastion.id
  }
}

# wariant 2: load balancer L4 na Virtual Routerze, z listą dozwolonych źródeł
resource "cloudstack_loadbalancer_rule" "api" {
  name = "kubernetes-apiserver"
  description = "Dostęp do API Kubernetesa"
  ip_address_id = cloudstack_ipaddress.public.id
  network_id = cloudstack_network.app.id
  algorithm = "roundrobin"
  private_port = 6443
  public_port = 6443
  protocol = "tcp"
  member_ids = cloudstack_instance.controlplane[*].id
  project = var.project

  cidrlist = var.admin_cidrs # kto w ogóle może dobić się do tej reguły
}

Atrybut cidrlist pojawił się w wydaniu 0.6.0 – na nim lub nowszym masz go od ręki. W starszych wersjach reguła load balancera jest otwarta na świat i musisz ją domykać osobno regułą firewalla.

Realny przykład: klaster Kubernetes opisany w całości kodem

Teraz najciekawsza część, czyli jak to wygląda w projekcie, który faktycznie stoi. U nas cały klaster Kubernetes na Talos Linux – dystrybucji przeznaczonej wyłącznie pod Kubernetesa, bez powłoki i bez SSH, konfigurowanej wyłącznie deklaratywnie przez API – jest opisany Terraformem od pustej strefy CloudStacka po działające ArgoCD.

Zanim pokażemy kod – uczciwa odpowiedź na pytanie, które zada każdy, kto zna CloudStacka: tak, CloudStack ma wbudowaną usługę Kubernetes (CloudStack Kubernetes Service), a oficjalny provider ma dla niej zasób cloudstack_kubernetes_cluster, obecny już w stabilnym wydaniu 0.6.0. Jeden zasób i klaster stoi. Dla wielu zastosowań to droga właściwa i nie ma powodu jej omijać.

Wybraliśmy dłuższą z trzech powodów. Po pierwsze, wersje Kubernetesa w usłudze wbudowanej to kolejne „menu operatora” – dostępne jest to, dla czego administrator chmury wgrał obraz; my chcieliśmy sterować wersją systemu węzłów i klastra jednym URL-em w pliku .tfvars. Po drugie, chcieliśmy własnego składu warstwy systemowej – Cilium jako CNI, Ceph RBD pod wolumeny trwałe, ArgoCD od pierwszego przebiegu – i tego samego wzorca również poza CloudStackiem. Po trzecie, Talos daje węzeł bez powłoki i bez SSH, czyli konfigurację, której nie da się „poprawić ręcznie na chwilę”.

Cena tego wyboru jest jawna: kilkaset linijek kodu więcej i utrzymanie warstwy, którą w usłudze wbudowanej prowadzi operator chmury – łącznie z aktualizacjami węzłów. Jeśli tego nie potrzebujesz, zacznij od usługi wbudowanej.

Układ repozytorium

├── modules/
│ ├── cloudstack/ # VPC, sieć, ACL, węzły, LB, szablon
│ └── talos/ # konfiguracja maszyn Talosa + patche
└── environments/
    ├── dev/
    └── prod/
        ├── backend.tf # stan w S3 (Ceph RGW)
        ├── providers.tf # cloudstack, talos, helm, kubectl, http
        ├── main.tf # spięcie całości
        ├── cloudstack.tfvars # wartości środowiska
        └── infra-deployments/
            ├── charts/ # cilium, CSI, CCM, prometheus, argocd
            └── argocd-apps/ # aplikacje GitOps

Podział jest klasyczny i sprawdza się dobrze: moduły opisują „jak”, katalogi środowisk opisują „ile i gdzie”. Różnica między dev a prod to w praktyce plik .tfvars – wartości niżej są przykładowe i pokazują, co w ogóle różnicuje środowiska, a nie rozmiary naszych klastrów:

# environments/prod/cloudstack.tfvars
zone = "PL-DCO-1"
project = "..." # UUID projektu CloudStacka

vpc_name = "Prod Redundant VPC"
vpc_cidr = "10.0.0.0/16"
vpc_offering = "Virtual Private Cloud with VR HA"
talos_network_name = "prod-network"
talos_network_cidr = "10.0.1.0/24"
talos_network_offering = "Isolated Network for VPC with SNAT"
talos_image_url = "https://factory.talos.dev/image/<hash>/<wersja>/cloudstack-amd64.raw.gz"

cluster_name = "prod"
controlplane_vip = "10.0.1.2"

W tym samym pliku siedzą jeszcze controlplane_count i worker_count – liczby węzłów są w praktyce jedynym, co zmienia się przy skalowaniu środowiska w górę albo w dół; konkretne wartości trzymamy w zmiennych środowiskowych, nie w treści modułu.

Szablon systemu prosto z URL-a

Pierwsza rzecz, która robi wrażenie na osobach przyzwyczajonych do ręcznego wgrywania obrazów: CloudStack potrafi zarejestrować szablon, pobierając go z dowolnego adresu HTTP, a Terraform potrafi tym sterować. Talos udostępnia gotowe obrazy dla CloudStacka przez usługę Image Factory, więc wersja systemu operacyjnego węzłów staje się… zmienną w pliku konfiguracyjnym:

resource "cloudstack_template" "talos" {
  name = "Talos-${regex("v[0-9]+\\.[0-9]+\\.[0-9]+", var.talos_image_url)}"
  format = "RAW"
  hypervisor = "KVM"
  os_type = "Other PV Virtio-SCSI (64-bit)"
  url = var.talos_image_url
  zone = var.zone
  project = var.project
}

Podbicie Talosa do nowszej wersji to zmiana jednego URL-a. Nazwa szablonu wyciąga z adresu numer wersji (tag postaci vX.Y.Z) wyrażeniem regularnym, więc nowa wersja tworzy nowy szablon zamiast po cichu nadpisywać stary.

Węzły: maszyna + konfiguracja w user_data

Talos nie ma SSH – całą konfigurację węzła dostaje w user_data przy pierwszym starcie. To bardzo dobrze współgra z Terraformem: moduł talos generuje konfiguracje maszyn, a moduł cloudstack wstrzykuje je do instancji:

resource "cloudstack_instance" "controlplane" {
  count = var.controlplane_count

  name = "${var.cluster_name}-control-${count.index + 1}"
  service_offering = "cs.custom.nvme"
  template = cloudstack_template.talos.id
  network_id = cloudstack_network.talos.id
  zone = var.zone
  project = var.project

  uefi = true
  root_disk_size = 200
  details = { cpuNumber = "2", cpuSpeed = "2000", memory = "16000" }

  user_data = base64gzip(var.talos_controlplane_config[count.index])

  expunge = true
}

base64gzip() zamiast base64encode() to nie ozdobnik – konfiguracja Talosa z patchami potrafi przekroczyć limit rozmiaru user_data.

expunge = true zostawiamy tu świadomie, mimo ostrzeżenia z poprzedniej sekcji: węzeł klastra jest wymienny i odtwarza się z tego samego kodu, a przed utratą klastra chroni kopia etcd, nie kosz CloudStacka. W maszynie, która trzyma dane, wybór byłby odwrotny.

Kolejność zdarzeń, czyli gdzie Terraform potrzebuje pomocy

Najciekawszy fragment całego projektu to main.tf środowiska, bo pokazuje coś, czego nie widać w tutorialach: stawianie klastra to sekwencja, w której część kroków musi zaczekać na rzeczywistość, a nie tylko na graf zależności Terraforma.

Zanim prześledzimy przebieg, jedna rzecz wymaga nazwania wprost: od tego miejsca w jednym katalogu pracuje kilka providerów naraz. cloudstack tworzy infrastrukturę, talos rozmawia z węzłami przez API Talosa (stąd zasoby o przedrostku talos_), helm i kubectl instalują komponenty w gotowym już klastrze, a http służy do jednego sprawdzenia gotowości. Zasobów talos_* nie szukaj więc w dokumentacji providera CloudStacka – nie ma ich tam i nie będzie. Terraform spina to wszystko w jednym przebiegu, bo wyjście jednego providera bywa wejściem następnego.

Przebieg wygląda tak:

  1. Powstaje VPC i rezerwowany jest publiczny adres IP.
  2. Moduł talos generuje konfiguracje maszyn – już znając publiczny endpoint klastra, bo adres jest znany przed utworzeniem węzłów.
  3. Moduł cloudstack tworzy szablon, sieć, ACL, węzły i reguły LB.
  4. talos_machine_configuration_apply wysyła konfigurację do każdego węzła.
  5. talos_machine_bootstrap inicjalizuje etcd – rozproszoną bazę, w której Kubernetes trzyma cały swój stan – na pierwszym węźle control plane; pozostałe węzły dołączają do niej same.
  6. Terraform czeka, aż API Kubernetesa naprawdę odpowie.
  7. Dopiero wtedy pobierany jest kubeconfig i instalowane są komponenty Helmem.

Krok szósty jest tym, którego brakuje w większości przykładów. Bootstrap kończy się zanim kube-apiserver zacznie obsługiwać ruch, więc kolejny zasób trafiłby w pustkę. Wzorzec wart zapamiętania – odpytywanie endpointu zdrowia z ponawianiem, jako zwykły data source:

data "http" "check_kubeapi" {
  depends_on = [talos_machine_bootstrap.cluster]

  url = "https://${cloudstack_ipaddress.lb.ip_address}:6443/healthz"
  insecure = true # świadomy skrót — certyfikat apiservera podpisuje CA klastra, nie publiczny urząd

  retry {
    attempts = 10
    min_delay_ms = 15000
    max_delay_ms = 15000 # stały odstęp: do 2,5 minuty cierpliwości
  }
}

resource "talos_cluster_kubeconfig" "cluster" {
  depends_on = [data.http.check_kubeapi] # tu jest cała sztuczka
  client_configuration = module.talos.talos_client_configuration
  node = cloudstack_ipaddress.lb.ip_address
}

To rozwiązanie zamiast time_sleep z wartością wziętą z sufitu: czeka dokładnie tyle, ile trzeba, a jeśli klaster nie wstanie – przerywa apply jasnym błędem, zamiast iść dalej po cichu. Jawny max_delay_ms nie jest tu nadmiarowy: bez niego provider bierze domyślny sufit 30 s i backoff wykładniczy, więc realne czekanie rośnie do niemal pięciu minut.

Jedno słowo o insecure = true, żeby nikt nie przeniósł tego nawyku dalej: to skrót, nie zalecenie. Sprawdzamy wyłącznie /healthz i nic tam nie wysyłamy, a certyfikat kube-apiservera jest podpisany CA klastra, więc domyślna weryfikacja i tak by go odrzuciła. Domknąć to jednak można – provider http przyjmuje ca_cert_pem, a CA Kubernetesa istnieje już wtedy w stanie Terraforma, bo generuje je talos_machine_secrets; wystarczy wystawić je jako wyjście modułu.

Warstwa kubernetesowa w tym samym przebiegu

Po pobraniu kubeconfiga ten sam apply konfiguruje providery helm i kubectl danymi z właśnie utworzonego klastra i instaluje warstwę systemową:

module "cilium" { source = "./infra-deployments/charts/cilium" }
module "kube-prometheus-stack" { source = "./infra-deployments/charts/kube-prometheus-stack" }
module "cloudstack-kubernetes-provider" { source = "./infra-deployments/charts/cloudstack-kubernetes-provider" /* ... */ }
module "cloudstack-csi" { source = "./infra-deployments/charts/cloudstack-csi" /* ... */ }
module "ceph_csi_rbd" { source = "./infra-deployments/charts/ceph-csi-rbd" /* ... */ }
module "argocd" { source = "./infra-deployments/charts/argocd" }
module "argocd-apps" { source = "./infra-deployments/argocd-apps" }

Trzy z tych komponentów warto omówić osobno:

  • cilium – CNI klastra, czyli wtyczka odpowiadająca za sieć między podami i polityki ruchu.
  • cloudstack-kubernetes-provider – cloud controller manager. Dzięki niemu Service typu LoadBalancer w Kubernetesie faktycznie zamawia w CloudStacku publiczny adres i regułę LB, zamiast wisieć wiecznie w stanie Pending.
  • cloudstack-csi – sterownik storage'u. Pozwala, by PersistentVolumeClaim tworzył prawdziwy wolumen CloudStacka i podpinał go do właściwego węzła.

Granica odpowiedzialności jest tu świadoma: Terraform stawia klaster i jego warstwę systemową, a od aplikacji przejmuje GitOps – model, w którym stan aplikacji jest opisany w repozytorium git, a agent działający w klastrze (u nas ArgoCD) stale porównuje go z rzeczywistością i usuwa różnice. Terraform jest doskonały w tworzeniu infrastruktury i kiepski jako pętla ciągłej rekoncyliacji aplikacji – mieszanie tych ról kończy się pipeline'ami trwającymi kwadrans.

Pipeline: plan automatycznie, apply za ręczną bramką

Całość jedzie w GitLab CI w układzie, który polecamy dla każdej infrastruktury produkcyjnej:

stages: [validate, plan, apply]

image:
  name: hashicorp/terraform:1.10.3
  entrypoint: [""]

before_script:
  - cd $BASE_PATH
  - terraform init
      -backend-config="access_key=$S3_ACCESS_KEY"
      -backend-config="secret_key=$S3_SECRET_KEY"

validate:
  stage: validate
  script: [terraform validate]

iac-scan: # statyczna analiza bezpieczeństwa kodu IaC
  stage: validate
  image:
    name: aquasec/trivy:latest
    entrypoint: [""] # bez tego GitLab nie uruchomi shella w tym obrazie
  before_script: [] # tu nie ma czego inicjalizować
  script: [trivy config --exit-code 1 $BASE_PATH]

plan:
  stage: plan
  script:
    - terraform plan -out=tfplan -var-file cloudstack.tfvars
    - terraform show -json tfplan > tfplan.json
  artifacts:
    paths: [$BASE_PATH/tfplan, $BASE_PATH/tfplan.json]
    access: developer # plan niesie te same sekrety co stan (GitLab >= 16.7)
    expire_in: 7 days # musi przeżyć ręczną bramkę przed `apply`

apply:
  stage: apply
  script: [terraform apply tfplan]
  when: manual
  dependencies: [plan]
  manual_confirmation: "Potwierdź, że chcesz wdrożyć te zmiany"

Artefakt planu jest tak samo wrażliwy jak plik stanu. Zapisany tfplan – a tym bardziej tfplan.json, bo terraform show -json wypisuje wartości oznaczone sensitive jawnie – zawiera te same sekrety co stan, razem z jego kopią sprzed zmiany. Artefakty GitLaba domyślnie może pobrać każdy, kto widzi joba, i leżą tygodniami, więc ogranicz dostęp (access: developer) i skróć retencję (expire_in) – pamiętając, że artefakt musi przeżyć ręczną bramkę przed apply, bo bez niego apply nie ma czego wdrożyć.

Trzy rzeczy, które robią tu robotę:

  • apply wykonuje zapisany plan, a nie liczy go od nowa – samo podanie pliku planu Terraform traktuje jako zatwierdzenie, więc -auto-approve jest tu zbędne. Wdrażasz dokładnie to, co ktoś przeczytał i zaakceptował.
  • Ręczna bramka z pytaniem potwierdzającym. Infrastruktura to nie miejsce na apply automatycznie po merge'u.
  • Skan bezpieczeństwa IaC w etapie walidacji. trivy config przechodzi po plikach .tf i wyłapuje typowe grzechy, zanim staną się rzeczywistością – u nas najczęściej sekrety wklejone w user_data. Dwie rzeczy warto tu wiedzieć. Po pierwsze: skanujemy Trivym, bo tfsec został do niego wchłonięty i nie dostaje już nowych reguł. Po drugie, uczciwie: gotowych reguł pisanych wprost pod provider CloudStacka jest niewiele – grubszy zestaw (otwarte na świat porty, wyłączone szyfrowanie) dotyczy providerów hiperskalerów. To i tak tania bramka, w tym samym duchu, który opisywaliśmy w tekście o stacku bezpieczeństwa open source.

Pułapki, które kosztują czas

Zebrane z praktyki, w kolejności od najczęstszych:

  1. Wersja providera. Funkcje przybywają wydaniami, a te wychodzą rzadko. Zanim uznasz, że „CloudStack tego nie umie”, sprawdź, czy nie umie tego Twoja wersja providera – API chmury zwykle ma daną funkcję od dawna.
  2. Zaszyte UUID-y. Skopiowanie identyfikatora sieci z panelu i wklejenie go do kodu działa – do pierwszej odbudowy środowiska. Wszystko, co da się znaleźć data source'em, znajduj data source'em; resztę trzymaj w zmiennych, nie w treści modułu. To zresztą klasyczny dług: sami mamy w kodzie jedno takie miejsce i wiemy, że kiedyś nas ugryzie.
  3. Asynchroniczność API. depends_on gwarantuje kolejność tworzenia zasobów, ale nie gotowość usługi. Tam, gdzie czekasz na rzeczywistość (API Kubernetesa, usługa w maszynie, propagacja DNS), użyj sprawdzenia z ponawianiem – jak data "http" z blokiem retry wyżej.
  4. ACL jako jeden zasób. Wszystkie reguły listy żyją w jednym zasobie Terraforma. Ręczna zmiana w panelu zostanie cofnięta przy najbliższym apply.
  5. Oferty typu „customized” wymagają pola details z cpuNumber, cpuSpeed i memory. Bez tego CloudStack odrzuci żądanie, a komunikat błędu nie zawsze wskaże powód wprost.
  6. expunge = true czyni destroy nieodwracalnym – maszyny nie odzyskasz ze stanu „Destroyed”. Bezpieczne tam, gdzie maszyna jest wymienna (jak węzeł klastra), ryzykowne wszędzie tam, gdzie trzyma dane.
  7. Load balancer jest warstwy czwartej i mieszka w Virtual Routerze. Nie planuj na nim routingu po ścieżkach ani wielkiego ruchu HTTP – od tego jest ingress w klastrze albo osobna maszyna z reverse proxy.
  8. Sekrety w stanie – i w planie. Klucze API, hasła i klucze Cepha lądują w tfstate jawnie, a razem z nim w zapisanym tfplan i w tfplan.json. Bucket ze stanami zabezpiecz jak magazyn haseł, a artefakt planu w CI traktuj identycznie: ograniczony dostęp i krótka retencja.
  9. Równoległe pipeline'y. Bez blokady stanu dwa jednoczesne apply potrafią rozjechać środowisko. use_lockfile = true kosztuje jedną linijkę.

CloudStack i Terraform w WebDisk Cloud

Nasza chmura publiczna stoi na Apache CloudStack z hypervisorem KVM i storage'em na klastrze Ceph – primary storage to pule RBD (osobno NVMe, osobno pojemnościowe), a Object Storage to RGW z API zgodnym z S3. Wszystko, co opisaliśmy wyżej, dotyczy więc tej samej platformy, na której możesz uruchomić własne środowisko.

Co to oznacza w praktyce:

  • Klucze API generujesz samodzielnie w panelu – nie trzeba ich od nas zamawiać. Tym samym kluczem obsłużysz Terraform, CloudMonkey i własne skrypty.
  • Bucket na stan Terraforma możesz mieć u nas obok maszyn – z natywną blokadą S3, bez dokładania jakiejkolwiek usługi z zewnątrz.
  • Nie ma tu warstwy pośredniej. Rozmawiasz z tym samym API CloudStacka, którego używa panel – a że jest to projekt Apache Software Foundation, Twój kod nie jest przywiązany do jednego dostawcy. To konkretny, mierzalny sposób ograniczania uzależnienia od dostawcy, o którym pisaliśmy szerzej osobno.
  • Zakres samoobsługi jest szerszy, niż się wydaje: VPC i tiery, listy ACL, publiczne adresy, przekierowania portów, reguły LB, szablony, snapshoty, wolumeny – wszystko to są zasoby Terraforma.

A jeśli tym, czego szukasz, jest gotowy klaster z opieką zamiast własnego Terraforma – opisany wyżej projekt jest dokładnie tym, co robimy w ramach wsparcia technicznego Kubernetes; o tym, dlaczego postawienie klastra to najłatwiejszy etap, pisaliśmy w osobnym artykule.

Częste pytania

Czym Apache CloudStack różni się od OpenStacka? Przede wszystkim złożonością i modelem wdrożenia. OpenStack to zbiór kilkudziesięciu współpracujących projektów z osobnymi API i cyklami życia – daje maksymalną elastyczność, ale wymaga zespołu utrzymującego samą platformę. CloudStack to jedna aplikacja Javy z jedną bazą MySQL, obejmująca cały model chmury: instalacja i aktualizacja są znacznie prostsze, kosztem mniejszej modułowości. Oba są projektami open source pod licencją Apache 2.0, oba mają providera Terraforma.

Czy do CloudStacka jest oficjalny provider Terraforma? Tak – cloudstack/cloudstack, rozwijany w repozytorium Apache. Trzeba jednak znać jego tempo: ostatnie stabilne wydanie w rejestrze to 0.6.0 z października 2025 roku, a gałąź 0.7.0 jest tam na razie obecna jako kandydat do wydania (0.7.0-rc1, sierpień 2026). Praktyczny wniosek: pinuj dokładną wersję, commituj .terraform.lock.hcl i zaglądaj do CHANGELOG-u – funkcja, której brakowało rok temu, mogła już wejść do wydania stabilnego.

Skąd wziąć klucze API do CloudStacka? Z panelu: Accounts → konto → Users → użytkownik → Generate Keys. Sekret jest pokazywany w całości tylko przy generowaniu. Dla automatyzacji załóż osobnego użytkownika technicznego z najmniejszymi potrzebnymi uprawnieniami – łatwiej go zrotować i wyśledzić w logu zdarzeń niż klucze przypisane do człowieka.

Gdzie trzymać plik stanu Terraforma, jeśli nie mam AWS-a? W dowolnym storage'u zgodnym z S3 – Ceph RGW, MinIO czy Object Storage u dostawcy. Backend s3 obsługuje własny endpoint, a od Terraforma 1.10 potrafi blokować stan natywnie, warunkowymi zapisami w S3 (use_lockfile = true), bez tabeli DynamoDB. Pamiętaj, że stan zawiera sekrety jawnym tekstem – bucket zabezpiecz odpowiednio.

Czy zamiast Terraforma mogę użyć OpenTofu? Tak. OpenTofu to fork Terraforma pod Linux Foundation, utworzony po zmianie licencji Terraforma na BUSL (Business Source License) – źródła pozostają jawne, ale formalnie nie jest to już licencja open source. Providery CloudStacka są zwykłymi pluginami i działają w obu narzędziach; konfiguracja pozostaje ta sama, zmienia się polecenie (tofu zamiast terraform). Jeśli licencja BUSL jest dla Ciebie problemem, to zwykle cała migracja.

Skoro CloudStack ma wbudowaną usługę Kubernetes (CKS), po co stawiać klaster własnym kodem? Nie ma po co, jeśli usługa wbudowana pokrywa Twój przypadek – oficjalny provider ma dla niej zasób cloudstack_kubernetes_cluster już w stabilnym wydaniu 0.6.0 i jest to najkrótsza droga do działającego klastra. Własny klaster (u nas na Talos Linuksie) wybiera się wtedy, gdy potrzebujesz kontroli nad wersją i konfiguracją węzłów, własnego zestawu CNI i sterowników storage'u albo tego samego wzorca na kilku platformach naraz. Cena to więcej kodu i pełna odpowiedzialność za warstwę, którą w usłudze wbudowanej utrzymuje operator chmury.

Dlaczego mój Service typu LoadBalancer w Kubernetesie na CloudStacku wisi w stanie Pending? Bo w klastrze brakuje komponentu, który zamówi adres i regułę w chmurze. Odpowiada za to cloudstack-kubernetes-provider, czyli cloud controller manager dla CloudStacka – bez niego Kubernetes nie ma jak poprosić o publiczny IP. Analogicznie za wolumeny odpowiada cloudstack-csi.

Czy load balancer CloudStacka zastąpi Application Load Balancer? Nie i nie warto go tak traktować. LB w CloudStacku działa w Virtual Routerze, jest balanserem warstwy czwartej z sesjami lepkimi i – od wydania 4.22 – z terminacją SSL na samym routerze, a jego przepustowość ogranicza pojedynczy router. Jeśli Twoja chmura stoi na starszym wydaniu, odciążenia SSL na routerze tam po prostu nie ma. Do routingu po ścieżkach, nagłówkach czy nazwach hostów użyj ingressu w klastrze Kubernetes albo własnego reverse proxy – CloudStack wtedy tylko dostarcza im publiczny adres.

Czy Terraform poradzi sobie z chmurą, która już istnieje? Tak, przez import istniejących zasobów do stanu (terraform import lub bloki import), ale bądź realistą: przy większym środowisku to praca na dni, nie na godziny, i wymaga cierpliwego dopasowywania kodu do rzeczywistości, aż plan przestanie proponować zmiany. Częściej opłaca się objąć kodem to, co nowe, a stare migrować przy okazji przebudowy.

Podsumowanie

Apache CloudStack ma cechę, która dobrze współgra z infrastrukturą jako kodem: jest zbudowany wokół jednego, spójnego API i jednego czytelnego modelu obiektowego. Strefa, oferta, sieć, VPC, ACL, szablon, projekt – to pojęcia, które równie dobrze opisują panel, jak i pliki .tf. Kiedy się je raz zrozumie, Terraform przestaje być tłumaczeniem między dwoma światami i staje się po prostu drugim sposobem powiedzenia tego samego.

Trzy rzeczy warto zabrać z tego tekstu. Po pierwsze: start jest tani – trzy wartości w konfiguracji providera, backend na S3, który prawdopodobnie już masz, i pierwsza maszyna w kilkunastu linijkach. Po drugie: skala się opłaca – cały klaster Kubernetes z siecią podów, storage'em i GitOps-em da się opisać kodem i odtworzyć jedną komendą, a różnica między dev a prod sprowadza się do pliku ze zmiennymi. Po trzecie: znaj ograniczenia – rzadkie wydania providera, oferty definiowane przez operatora, asynchroniczne API i load balancer warstwy czwartej to nie usterki, tylko właściwości platformy, które trzeba uwzględnić przy projektowaniu środowiska.

Jeśli zaczynasz od zera, dobra kolejność jest taka: wygeneruj klucze API, opisz jedną maszynę i jedną sieć, przenieś stan do S3, dołóż pipeline z ręczną bramką przed apply, a dopiero potem sięgaj po moduły i środowiska. Każdy z tych kroków ma sens samodzielnie – i żaden nie wymaga przepisania tego, co już działa.

Zastanawiasz się, czy Twoje środowisko da się opisać kodem i od czego zacząć? Napisz do naszespół WebDisk chętnie przejdzie z Tobą przez pierwszy moduł.