Pierwsze kroki z IaC: porównanie Terraform, Ansible i Pulumi na prostych przykładach

0
32
2/5 - (1 vote)

Nawigacja:

Po co w ogóle IaC? Zanim wybierzesz narzędzie

Celem jest zbudowanie pierwszej infrastruktury jako kod i zrozumienie, czy lepiej sprawdzi się Terraform, Ansible czy Pulumi. Zanim jednak przełączysz się na konkretne narzędzie, pojawia się ważniejsze pytanie: co dokładnie chcesz automatyzować i w jaki sposób dziś to robisz?

Krótka definicja infrastruktury jako kod (IaC)

Infrastruktura jako kod (Infrastructure as Code, IaC) to podejście, w którym serwery, sieci, bazy danych, load balancery i cała reszta infrastruktury są opisywane w plikach tekstowych. Te pliki trafiają do Git, przechodzą code review, są testowane i automatycznie wdrażane – tak jak aplikacja.

Bez IaC nową maszynę czy środowisko tworzysz „ręcznie”: klikasz w panelu chmury, wpisujesz komendy na SSH, utrzymujesz checklisty w Notion lub Excelu. Z IaC ten sam proces staje się powtarzalnym kodem, który można uruchomić wielokrotnie i w różnych środowiskach (dev, test, prod), z przewidywalnym wynikiem.

Kluczową zmianą jest przesunięcie odpowiedzialności: infrastruktura przestaje żyć „w głowie admina” i zaczyna żyć w repozytorium Git. Dzięki temu jest widoczna dla całego zespołu, można do niej stosować standardowe praktyki inżynieryjne, jak code review, testy i versioning.

Typowe bóle bez IaC: gdzie dziś tracisz czas?

Przyjrzyj się swojemu obecnemu sposobowi pracy. Jak powstaje nowy serwer albo środowisko testowe? Czy scenariusz wygląda mniej więcej tak:

  • ktoś klika w konsoli AWS/Azure/GCP, tworzy maszynę „na czuja”;
  • część konfiguracji jest w starym dokumencie, część w pamięci admina;
  • po kilku miesiącach nikt nie wie, dlaczego ta maszyna ma taki rozmiar i takie reguły firewalli;
  • odtworzenie środowiska prod → test zajmuje godziny lub dni i nigdy nie jest identyczne.

Jeśli chcesz sam siebie zdiagnozować, zadaj sobie kilka prostych pytań:

  • gdyby jutro skasować środowisko testowe, ile czasu zajęłoby odtworzenie go „jeden do jednego”?
  • czy wiesz, co się zmieniło w infrastrukturze w ciągu ostatnich 30 dni?
  • czy konfiguracja jest udokumentowana w Git, czy raczej w Slacku i głowach zespołu?

Jeśli na większość z tych pytań odpowiedź brzmi „nie wiem” lub „to byłby dramat” – to właśnie jest przestrzeń, którą IaC ma uporządkować.

Co realnie zyskujesz wdrażając IaC?

Przejście na infrastrukturę jako kod daje kilka konkretnych efektów, odczuwalnych już przy małych projektach:

Powtarzalność i odtwarzalność – ten sam kod Terraform, Ansible lub Pulumi możesz uruchomić na dev, stage, prod z różnymi parametrami. Nie musisz pamiętać, co kiedyś kliknąłeś w panelu. Gdy padnie cała strefa chmurowa lub serwerownia, odtwarzasz środowisko z kodu, nie z domysłów.

Kontrola zmian – każda modyfikacja infrastruktury to commit i pull request. Można porównać diff, zobaczyć kto, kiedy i po co zmienił daną regułę firewall. W wielu organizacjach samo to ogranicza ilość „magicznych” zmian wykonywanych na produkcji w piątek po południu.

Bezpieczne eksperymentowanie – chcesz przetestować nowy typ maszyny, inny layout sieci albo inny provider? Tworzysz nowy branch, ewentualnie osobny stack/projekt, i odpalasz całość obok. Gdy eksperyment się nie sprawdzi, usuwasz infrastrukturę jedną komendą.

Lepsze współdzielenie wiedzy – nowa osoba w zespole, zamiast uczyć się z chaotycznych notatek, bierze repozytorium z IaC, czyta moduły i od razu widzi wysokopoziomową architekturę. To mocno skraca onboarding.

Deklaratywne vs imperatywne podejście – po której stronie jesteś?

W IaC często pojawia się rozróżnienie:

  • podejście deklaratywne – mówisz „jak ma wyglądać stan końcowy” (np. chcę mieć 1 VM z takimi parametrami), a narzędzie samo wymyśla, co zmienić;
  • podejście imperatywne – mówisz „jak wykonać kroki” (np. stwórz maszynę, potem doinstaluj pakiety, potem zrestartuj usługę).

Terraform i Pulumi działają głównie deklaratywnie – opisujesz docelową infrastrukturę. Ansible jest po środku: deklaruje stan (np. pakiet X ma być zainstalowany), ale wciąż myślisz krokami i sekwencją.

Zastanów się, co jest ci bliższe: opisywanie jak ma być, czy jak dojść do tego stanu? Odpowiedź będzie ważna przy wyborze narzędzia.

Jak dziś tworzysz i utrzymujesz infrastrukturę?

Kluczowe pytanie na start: jaki masz dziś sposób na tworzenie i utrzymanie infrastruktury? Czy wygląda to bardziej jak:

  • ręczne klikanie w konsoli chmurowej;
  • skrypty bash/powershell z porozrzucanymi parametrami;
  • częściowe użycie Ansible/CloudFormation/Terraform, ale bez spójnego procesu;
  • mieszanka wszystkiego naraz.

Druga sprawa: co najbardziej boli? Zbyt długie tworzenie środowisk? Różnice między dev a prod? Brak historii zmian? Odpowiedź na te pytania pomoże dobrać, czy ważniejsze jest:

  • szybkie i bezpieczne tworzenie zasobów w chmurze (Terraform, Pulumi),
  • czy raczej spójna konfiguracja systemów i aplikacji (Ansible),
  • czy może jedno i drugie, ale etapami.

Gdzie IaC wpisuje się w nowoczesną infrastrukturę i chmurę

Infrastruktura jako kod nabiera sensu dopiero w szerszym kontekście: chmury, wirtualizacji, kontenerów i pipeline’ów CI/CD. Bez tego łatwo wrzucić IaC jako „narzędzie do klikania bez klikania”, zamiast centralnego elementu procesu.

IaC w chmurze publicznej i środowiskach hybrydowych

W AWS, Azure czy GCP zasobów jest wiele: sieci VPC, podsieci, role IAM, kontenery, bazy danych, kolejki, load balancery. Tworzenie tego ręcznie jest uciążliwe już przy jednym środowisku, a przy kilku – trudne do utrzymania.

IaC (Terraform, Pulumi, w pewnym stopniu Ansible) staje się wtedy warstwą opisującą chmurę. Niezależnie, czy tworzysz:

  • proste środowisko z jedną aplikacją webową,
  • złożony system z wieloma mikroserwisami, kilkoma VPC i peeringami,
  • infrastrukturę hybrydową, gdzie część zasobów jest w on-prem, a część w AWS,

– podejście IaC pozwala trzymać całą tę złożoność w wersjonowanym kodzie.

Jeśli działasz w modelu hybrydowym, IaC porządkuje też komunikację z działem infrastruktury on-prem. Zamiast długich maili „stworzyć VLAN X, potem otworzyć port Y”, przekazujesz opisany kod lub moduł, który integruje się z ich narzędziami.

Integracja IaC z wirtualizacją, Dockerem i Kubernetesem

Masz już Dockera, może pierwsze klastry Kubernetes? IaC pomaga połączyć to w całość. W praktyce warstwy układają się mniej więcej tak:

  • Warstwa infrastruktury – sieci, maszyny wirtualne, load balancery, klastry Kubernetes (np. EKS/AKS/GKE). Tu najczęściej pracuje Terraform lub Pulumi.
  • Warstwa platformy – sam Kubernetes, ingressy, storage classes. Część zespołów opisuje to IaC (Terraform, Pulumi), część używa manifestów YAML + Helm.
  • Warstwa aplikacji – kontenery, deploymenty, konfiguracja aplikacji. Tu wchodzą pipeline’y CI/CD, Helm, ArgoCD, Ansible (jeśli mówimy o klasycznych VM-ach).

Jeśli dziś większość twoich usług to kontenery w Kubernetes, IaC będzie dotykać głównie warstwy poniżej K8s (sieci, klastry, bazy) oraz częściowo manifestów. Jeśli dopiero startujesz i masz głównie VM, IaC przejmie tworzenie maszyn i wstępną konfigurację.

GitOps i IaC: repozytorium jako źródło prawdy

GitOps to podejście, w którym Git jest jedynym źródłem prawdy o stanie systemu. Nie ma „ukrytych zmian” w konsoli chmurowej. Stan infrastruktury wynika z tego, co jest w repozytorium.

Jak IaC łączy się z GitOps?

  • kod Terraform/Pulumi/Ansible trafia do repo,
  • każda zmiana przechodzi przez pull request i review,
  • pipeline CI/CD uruchamia narzędzie IaC (plan/apply/up),
  • realny stan produkcji jest odzwierciedleniem zmergowanego kodu.

Jeżeli dziś często zdarza się, że ktoś „naprawia” coś na produkcji ręcznie, a potem to znika przy kolejnym wdrożeniu – GitOps ogranicza takie sytuacje. Kluczowe pytanie: czy chcesz, aby infrastrukturę dało się zmienić tylko przez zmianę kodu? Jeśli tak, IaC + GitOps to naturalny kierunek.

Provisioning vs konfiguracja – rozdzielenie odpowiedzialności

Dobrze jest jasno odseparować dwa etapy:

  • provisioning – tworzenie zasobów: maszyn wirtualnych, sieci, baz danych, load balancerów;
  • konfiguracja – to, co dzieje się na tych zasobach: system operacyjny, pakiety, konfiguracja aplikacji, deploy wersji.

Terraform i Pulumi radzą sobie świetnie z provisioningiem. Ansible jest najmocniejszy w konfiguracji. Można mieszać podejścia, np.: Terraform tworzy VM, Ansible konfiguruje na nich Nginx i aplikację.

Zastanów się, czy twoim celem jest głównie:

  • zapanowanie nad zasobami w chmurze (VPC, VM, bazy) – wtedy punkt ciężkości to Terraform/Pulumi,
  • czy uporządkowanie tego, co dzieje się na serwerach (system, aplikacje) – wtedy naturalnie ciąży się w stronę Ansible.
Robotyczna dłoń sięgająca do jasnego światła na białym tle
Źródło: Pexels | Autor: Tara Winstead

Różne klasy narzędzi: gdzie na osi leżą Terraform, Ansible i Pulumi

Terraform, Ansible i Pulumi często lądują w jednym worku „narzędzia IaC”, ale mają różne DNA. Wybór między nimi zależy od tego, czy bliżej ci do programisty, czy do admina/ops, oraz co chcesz zautomatyzować w pierwszej kolejności.

Terraform – deklaratywny provisioning infrastruktury

Terraform opisuje infrastrukturę w języku HCL (HashiCorp Configuration Language). Jego główne cechy:

  • deklaratywny opis stanu – mówisz „chcę mieć 2 VM, 1 VPC, 1 bazę danych”, Terraform sam decyduje, co stworzyć, zaktualizować, usunąć;
  • silne wsparcie chmur publicznych – providerzy dla AWS, Azure, GCP, ale też dla wielu SaaS (Cloudflare, Datadog itd.);
  • pliki stanu – Terraform przechowuje informacje o tym, co utworzył, w tzw. state file, dzięki czemu umie wykrywać zmiany i drift.

Terraform jest idealny, gdy chcesz ogarnąć provisioning: sieci, maszyny, bazy, load balancery. Do konfiguracji systemu (instalacja Nginx, ustawienia aplikacji) zwykle łączy się go z innymi narzędziami (cloud-init, Ansible, skrypty).

Ansible – konfiguracja, orkiestracja, a czasem provisioning

Ansible to narzędzie pisane z myślą o konfiguracji systemów i orkiestracji. Kluczowe cechy:

  • agentless – nie wymaga instalowania agenta na hostach; łączy się po SSH (Linux) lub WinRM (Windows);
  • YAML i moduły – definiujesz playbooki w YAML, korzystając z gotowych modułów (np. apt, service, user, a także modułów do chmury);
  • idempotencja – dobrze napisane playbooki można uruchamiać wielokrotnie, a Ansible wykona tylko konieczne zmiany.

Ansible ma też moduły do tworzenia zasobów w chmurze (np. EC2, VPC w AWS), ale praca z większą infrastrukturą bywa mniej wygodna niż w Terraform. Jeśli twoim głównym bólem jest chaotyczna konfiguracja serwerów – Ansible daje najszybszy efekt.

Pulumi – IaC jako „normalny” kod aplikacyjny

Pulumi, w odróżnieniu od Terraform, pozwala opisywać infrastrukturę przy użyciu pełnoprawnych języków programowania (TypeScript/JavaScript, Python, Go, .NET). Kluczowe wyróżniki:

  • re-używalność kodu – korzystasz z funkcji, klas, modułów jak w typowym projekcie;
  • pełne konstrukcje językowe – pętle, warunki, typy, testy jednostkowe; łatwiej modelować skomplikowane zależności;
  • Gdzie Pulumi pasuje w ekosystemie IaC

    Pulumi najłatwiej zrozumieć, porównując je na osi między Terraformem a „normalnym” kodem aplikacyjnym. Zastanów się: bliżej ci do YAML‑owych deklaracji, czy do pisania funkcji, klas, testów?

  • Jeśli lubisz Terraform, ale brakuje ci funkcji, typów, łatwej logiki warunkowej – Pulumi daje to od razu, bez sztuczek.
  • Jeśli jesteś developerem i IaC ma być „po prostu kolejnym projektem” – Pulumi pozwala użyć tych samych narzędzi: npm/pip, IDE, lintery, testy.
  • Jeśli bardziej myślisz jak admin/ops, który woli proste deklaracje niż złożone konstrukcje językowe – próg wejścia w Pulumi może być wyższy niż w Terraform.

Pytanie pomocnicze: czy twoja infrastruktura ma dużo powtarzalnych wzorców, które chcesz opakować w funkcje i biblioteki? Jeżeli tak, Pulumi może dać największy zwrot z inwestycji.

Terraform w praktyce – pierwszy prosty przykład

Czas przejść do kodu. Załóżmy, że chcesz stworzyć minimalne środowisko w chmurze: jedną maszynę wirtualną i sieć dookoła niej. Skupmy się na AWS, ale schemat będzie podobny dla innych chmur.

Założenia i przygotowanie środowiska Terraform

Zanim powstanie pierwszy plik, zadaj sobie pytanie: czy to ma być jednorazowy eksperyment, czy początek repozytorium produkcyjnego? Od tego zależy, czy od razu zadbasz o strukturę katalogów, zdalny state, moduły.

Do najprostszego startu wystarczy:

  • zainstalowany Terraform (binarka na lokalnym komputerze lub w CI),
  • konto w chmurze (np. AWS) i skonfigurowane poświadczenia (aws configure lub zmienne środowiskowe),
  • pusty katalog projektu, np. terraform-demo/.

Na początek użyjemy pojedynczego pliku main.tf. Później łatwo go rozbijesz na mniejsze.

Minimalna konfiguracja: provider i jedna maszyna

Zacznij od zdefiniowania providera i prostego zasobu EC2. Fragment przykładowego main.tf może wyglądać tak:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  required_version = ">= 1.5.0"
}

provider "aws" {
  region = "eu-central-1"
}

resource "aws_instance" "web" {
  ami           = "ami-0c8e96a3bd6184f15" # przykładowy AMI dla Ubuntu w eu-central-1
  instance_type = "t3.micro"

  tags = {
    Name = "demo-web"
  }
}

Kilka rzeczy jest tu kluczowych:

  • terraform – blok meta; deklarujesz, jakich providerów używasz i jakiej wersji Terraform wymagają.
  • provider "aws" – konfiguracja połączenia z AWS; region możesz wynosić do zmiennych.
  • resource "aws_instance" "web" – zasób: typ aws_instance, nazwa logiczna web. To ta nazwa pojawi się w odwołaniach, nie w samym AWS.

Zadaj sobie teraz pytanie: czy wiesz, skąd bierze się wartość AMI? Jeśli nie – zatrzymaj się i znajdź konkretny obraz (np. w konsoli AWS lub katalogu AMI). Błędne AMI to najczęstszy „pierwszy błąd” przy pracy z Terraform.

Inicjalizacja i pierwszy plan

Gdy masz plik main.tf, przejdź do katalogu projektu i wykonaj:

terraform init

Komenda pobierze plugin providera AWS i przygotuje katalog roboczy. Potem sprawdzasz, co Terraform zamierza zrobić:

terraform plan

Plan pokaże propozycję zmian (dodanie jednej instancji EC2). To moment, żeby zadać sobie pytanie: czy rozumiesz każdą linię, która ma powstać? Jeżeli nie, nie klikaj „apply” na ślepo – wróć do pliku i uprość konfigurację.

Kiedy plan wygląda sensownie, wdrażasz zmiany:

terraform apply

Po potwierdzeniu (lub dodaniu -auto-approve) Terraform utworzy instancję. Plik terraform.tfstate pojawi się w katalogu – to lokalny stan infrastruktury.

Dodanie prostego VPC i bezpieczeństwa

Samotna maszyna w domyślnej sieci rzadko ma sens. W kolejnym kroku dodasz VPC, podsieć i grupę bezpieczeństwa. Przykładowe zasoby:

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "demo-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "demo-subnet-public"
  }
}

resource "aws_security_group" "web_sg" {
  name        = "demo-web-sg"
  description = "Allow HTTP and SSH"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "demo-web-sg"
  }
}

Następnie powiąż instancję z siecią:

resource "aws_instance" "web" {
  ami           = "ami-0c8e96a3bd6184f15"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  tags = {
    Name = "demo-web"
  }
}

Teraz plik zaczyna przypominać „prawdziwą” infrastrukturę. Zadaj sobie pytanie: czy potrafiłbyś odtworzyć te zasoby ręcznie w konsoli AWS? Jeśli tak, rozumiesz, co robi Terraform. Jeśli nie – eksploracja konsoli ułatwi późniejsze debugowanie.

Wyjścia i parametryzacja – pierwsze kroki do „używalnego” kodu

Sam provisioning to nie wszystko. Przydatne jest zwrócenie kluczowych informacji, np. publicznego IP:

output "web_public_ip" {
  value = aws_instance.web.public_ip
}

Po apply Terraform wypisze IP, którego możesz użyć do połączenia. To wygodny sposób na przekazywanie danych do kolejnych narzędzi (np. Ansible, skryptów CI).

Kolejna myśl: czy wartości, których używasz, są „na sztywno” czy będą się różnić między środowiskami? Jeśli planujesz wiele środowisk, już na starcie pomyśl o zmiennych:

variable "aws_region" {
  type        = string
  default     = "eu-central-1"
  description = "Region AWS"
}

provider "aws" {
  region = var.aws_region
}

Dalsza parametryzacja (typu rozmiar instancji, CIDR, ilość VM) to kolejny krok – uzależnij jego tempo od tego, jak rozbudowane środowisko chcesz budować.

Nowoczesna szafa serwerowa w centrum danych z okablowaniem i sprzętem
Źródło: Pexels | Autor: Sergei Starostin

Ansible w praktyce – prosty playbook od zera

Terraform stworzył infrastrukturę. Teraz przychodzi kolej na to, co dzieje się na serwerze. Masz już cel? Np. prosty serwer HTTP z Nginx, który pokaże statyczną stronę.

Założenia: co chcemy skonfigurować

Przykładowy scenariusz:

  • system: Ubuntu na VM utworzonej wcześniej (np. aws_instance.web),
  • zadanie: zainstalować Nginx, włączyć usługę, podmienić stronę startową,
  • tryb pracy: lokalny Ansible łączący się po SSH do jednego hosta.

Jeśli nie używasz AWS, możesz zastosować to samo dla dowolnej maszyny, która jest osiągalna po SSH. Kluczowe pytanie: czy możesz zalogować się na tę maszynę ręcznie? Jeżeli nie, najpierw rozwiąż dostęp (klucze, firewall, użytkownicy).

Prosty inventory i pierwszy playbook

Najpierw inventory. W katalogu projektu Ansible utwórz plik hosts.ini:

[web]
1.2.3.4 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa

Zastąp 1.2.3.4 adresem IP swojej maszyny. Potem utwórz najprostszy playbook site.yml:

---
- name: Konfiguracja serwera web
  hosts: web
  become: true

  tasks:
    - name: Zaktualizuj cache pakietów APT
      ansible.builtin.apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Zainstaluj Nginx
      ansible.builtin.apt:
        name: nginx
        state: present

    - name: Upewnij się, że Nginx jest włączony i działa
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: yes

    - name: Podmień domyślną stronę index.html
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        content: |
          <html>
            <head><title>Demo Ansible</title></head>
            <body><h1>Działa!</h1></body>
          </html>
        owner: www-data
        group: www-data
        mode: "0644"

Widzisz tu kilka cech Ansible w pigułce:

  • moduły (apt, service, copy) zamiast surowych komend,
  • idempotencja – powtórne odpalenie playbooka nie zepsuje konfiguracji, tylko ją utrwali,
  • czytelny opis w języku zbliżonym do naturalnego.

Uruchom playbooka poleceniem:

ansible-playbook -i hosts.ini site.yml

Jeśli wszystko poszło dobrze, wejście na http://<IP>/ powinno pokazać nową stronę. Pytanie do ciebie: czy wszystkie zadania są dla ciebie zrozumiałe, czy kopiujesz je „na wiarę”? Jeśli drugie – spróbuj odkomentować pojedyncze zadania i obserwować, co robią.

Porządniejsze struktury: roles i zmienne

Ten przykład jest celowo płaski. W praktyce Playbook szybko rośnie, więc przychodzi moment na podział na role. Dla prostego serwera Nginx struktura może wyglądać tak:

ansible/
  hosts.ini
  site.yml
  roles/
    webserver/
      tasks/
        main.yml
      templates/
        index.html.j2
      vars/
        main.yml

W site.yml zamiast listy zadań używasz roli:

---
- name: Konfiguracja serwera web
  hosts: web
  become: true

  roles:
    - webserver

W roles/webserver/tasks/main.yml lądują znane już zadania, ewentualnie rozbite na mniejsze pliki. W templates/index.html.j2 możesz użyć Jinja2 i zmiennych:

<html>
  <head><title>{{ app_title }}</title></head>
  <body><h1>Witaj na {{ app_title }}</h1></body>
</html>

W vars/main.yml określasz wartości:

---
app_title: "Demo aplikacja"

Dzięki takiej strukturze możesz wielokrotnie używać tej samej roli w różnych środowiskach. Pytanie do zastanowienia: czy twoje serwery różnią się tylko kilkoma parametrami, czy mają zupełnie inne role? Od tego zależy, jak szczegółowo dzielić konfigurację.

Łączenie Ansible z Terraformem

Bardzo częsty wzorzec to: Terraform tworzy serwery, Ansible je konfiguruje. Można to zrobić na kilka sposobów:

  • ręcznie – po terraform apply kopiujesz IP do inventory Ansible,
  • półautomatycznie – Terraform generuje plik inventory (np. przez local_file), a Ansible z niego korzysta,
  • dynamicznie – Ansible używa dynamicznego inventory dla chmury (np. AWS inventory plugin).

Zadaj sobie pytanie: jak często będziesz tworzyć/usuwać serwery? Jeżeli sporadycznie, ręczny krok bywa wystarczający. Jeśli serwery będą częścią codziennego cyklu CI/CD, automatyzacja przekazania danych między Terraform a Ansible szybko się zwróci.

Pulumi w praktyce – pierwsza infrastruktura jako „normalny” kod

Co wyróżnia Pulumi na tle Terraform i Ansible

Pulumi to też IaC, ale pisane w „normalnych” językach programowania: TypeScript/JavaScript, Python, Go, C#, Java. Zamiast deklaratywnego HCL (Terraform) masz pełnoprawny kod z pętlami, funkcjami, testami jednostkowymi.

Zanim zaczniesz, zadaj sobie pytanie: w jakim języku czujesz się najbardziej swobodnie? Jeśli odpowiedź brzmi „żadnym” – Terraform z HCL będzie prostszy na start. Jeśli jednak używasz na co dzień np. Pythona albo TypeScriptu, Pulumi daje przewagę: możesz traktować infrastrukturę jak normalną bibliotekę w projekcie.

Punkty, które mocno rzucają się w oczy przy pierwszym kontakcie:

  • programistyczny model – możesz tworzyć abstrakcje (funkcje, klasy, moduły) zamiast kopiować fragmenty HCL,
  • ten sam język dla aplikacji i infrastruktury – np. backend w Node.js i IaC w TypeScript w jednym repozytorium,
  • łatwiejsza integracja z istniejącymi bibliotekami (np. pobranie tajnych danych z zewnętrznego API przed utworzeniem zasobów).

Pytanie kontrolne: czy twoja infrastruktura będzie prostą siecią i kilkoma VM, czy rozwiniętym ekosystemem mikroserwisów? Jeśli to drugie, programistyczny model może dać ci sporą przewagę przy wzroście złożoności.

Minimalny projekt Pulumi w TypeScript – od instalacji do pierwszego stacka

Załóżmy, że znasz przynajmniej podstawy Node.js/TypeScript. Zacznij od zainstalowania Pulumi CLI:

curl -fsSL https://get.pulumi.com | sh
# lub przez menedżera pakietów, np. brew install pulumi

Następnie utwórz katalog projektu i zainicjalizuj stack:

mkdir pulumi-demo
cd pulumi-demo
pulumi new aws-typescript

Kreator zada kilka pytań: nazwę projektu, nazwę stacka (np. dev), region AWS itp. Zatrzymaj się przy każdym i zapytaj sam siebie: czy będę mieć więcej niż jedno środowisko? Jeśli tak, od razu załóż, że powstanie też staging i prod jako oddzielne stacki.

Po zakończeniu inicjalizacji zobaczysz pliki m.in.:

  • index.ts – główny „program infrastrukturalny”,
  • Pulumi.dev.yaml – konfiguracja stacka dev,
  • package.json – standardowa konfiguracja projektu Node.js.

Prosty przykład: analog Terraformowego VPC + instancja EC2

Spójrz na analogię do kodu Terraform, który tworzył VPC, subnet, SG i instancję. W Pulumi możesz zapisać to tak (upraszczając, bez wszystkich parametrów):

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

// VPC
const vpc = new aws.ec2.Vpc("demo-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsSupport: true,
    enableDnsHostnames: true,
    tags: {
        Name: "demo-vpc",
    },
});

// Subnet publiczny
const publicSubnet = new aws.ec2.Subnet("demo-subnet-public", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    mapPublicIpOnLaunch: true,
    tags: {
        Name: "demo-subnet-public",
    },
});

// Security group dla HTTP + SSH
const webSg = new aws.ec2.SecurityGroup("demo-web-sg", {
    vpcId: vpc.id,
    description: "Allow HTTP and SSH",
    ingress: [
        { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] },
        { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
    ],
    egress: [
        { protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
    ],
    tags: {
        Name: "demo-web-sg",
    },
});

// Instancja EC2
const web = new aws.ec2.Instance("demo-web", {
    ami: "ami-0c8e96a3bd6184f15",
    instanceType: "t3.micro",
    subnetId: publicSubnet.id,
    vpcSecurityGroupIds: [webSg.id],
    tags: {
        Name: "demo-web",
    },
});

// Eksport publicznego IP jako output
export const webPublicIp = web.publicIp;

Jeżeli rozumiesz wcześniejszy przykład Terraform, ten kod nie powinien być zaskoczeniem. Różnica jest w stylistyce i możliwościach. Obiekt webPublicIp to normalny eksport, który możesz potem użyć w innych częściach programu, testach czy aplikacji.

Uruchomienie jest podobne jak w Terraform:

# podgląd zmian
pulumi preview

# zastosowanie zmian
pulumi up

# usunięcie zasobów
pulumi destroy

Na tym etapie warto odpowiedzieć sobie na pytanie: czy łatwiej ci śledzić zależności w kodzie TypeScript, czy w deklaracjach HCL? Odpowiedź podpowie, czy Pulumi ma sens w twoim kontekście.

Parametryzacja Pulumi: Config zamiast „gołych” zmiennych

W Terraform używałeś variable i tfvars. W Pulumi odpowiednikiem jest konfiguracja stacka. Przykład w TypeScript:

const config = new pulumi.Config();

const awsRegion = config.get("awsRegion") || "eu-central-1";
const instanceType = config.get("instanceType") || "t3.micro";

Ustawienie wartości dla stacka dev:

pulumi config set awsRegion eu-central-1
pulumi config set instanceType t3.small

W pliku Pulumi.dev.yaml zobaczysz zapisane wartości. Dla różnych stacków (np. staging, prod) możesz mieć inne regiony, typy instancji, rozmiary baz danych.

Dodatkowe pytanie: czy już teraz planujesz co najmniej dwa stacki (np. dev i prod)? Jeżeli tak, od początku myśl o tym, co powinno być parametrem, a co na stałe wpisanym elementem architektury.

Łączenie Pulumi z konfiguracją wewnątrz instancji

Pojawia się naturalne pytanie: co z częścią, którą robił Ansible? Masz kilka możliwości:

  • user data (cloud-init) w instancji – proste skrypty bash, np. instalacja Nginx,
  • uruchamianie Ansible po utworzeniu zasobów (podobnie jak z Terraform),
  • bezpośrednia konfiguracja z Pulumi (np. przez provisionery, skrypty, inne zasoby).

Przykład z użyciem userData w instancji EC2 w Pulumi (TypeScript):

const userData = `#!/bin/bash
apt-get update -y
apt-get install -y nginx
cat > /var/www/html/index.html << 'EOF'
<html>
  <head><title>Pulumi Demo</title></head>
  <body><h1>Działa z Pulumi!</h1></body>
</html>
EOF
systemctl enable nginx
systemctl restart nginx
`;

const web = new aws.ec2.Instance("demo-web", {
    ami: "ami-0c8e96a3bd6184f15",
    instanceType: instanceType,
    subnetId: publicSubnet.id,
    vpcSecurityGroupIds: [webSg.id],
    userData: userData,
    tags: {
        Name: "demo-web",
    },
});

Takie podejście działa dobrze dla bardzo prostych scenariuszy. Gdy konfiguracja rośnie, pojawia się pytanie: czy chcesz debugować długie skrypty bash wplecione w kod TypeScript? Jeśli nie, lepszy bywa osobny krok z Ansible lub innym narzędziem do konfiguracji.

Reużywalność: funkcja budująca zestaw zasobów

Jedna z największych zalet Pulumi pojawia się wtedy, gdy masz powtarzalny wzorzec infrastruktury: np. kilka podobnych środowisk aplikacyjnych różniących się nazwą i rozmiarem. Zamiast kopiować kod, tworzysz funkcję:

interface WebStackArgs {
    name: string;
    cidrBlock: string;
    instanceType: string;
}

export function createWebStack(args: WebStackArgs) {
    const vpc = new aws.ec2.Vpc(`${args.name}-vpc`, {
        cidrBlock: args.cidrBlock,
        enableDnsSupport: true,
        enableDnsHostnames: true,
    });

    const subnet = new aws.ec2.Subnet(`${args.name}-subnet-public`, {
        vpcId: vpc.id,
        cidrBlock: "10.0.1.0/24",
        mapPublicIpOnLaunch: true,
    });

    const sg = new aws.ec2.SecurityGroup(`${args.name}-sg`, {
        vpcId: vpc.id,
        ingress: [
            { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] },
            { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
        ],
        egress: [
            { protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
        ],
    });

    const instance = new aws.ec2.Instance(`${args.name}-web`, {
        ami: "ami-0c8e96a3bd6184f15",
        instanceType: args.instanceType,
        subnetId: subnet.id,
        vpcSecurityGroupIds: [sg.id],
    });

    return {
        vpcId: vpc.id,
        publicIp: instance.publicIp,
    };
}

W index.ts możesz teraz tworzyć różne środowiska na podstawie tej samej „fabryki”:

const config = new pulumi.Config();

const dev = createWebStack({
    name: "dev",
    cidrBlock: "10.10.0.0/16",
    instanceType: config.get("devInstanceType") || "t3.micro",
});

const staging = createWebStack({
    name: "stg",
    cidrBlock: "10.20.0.0/16",
    instanceType: config.get("stgInstanceType") || "t3.small",
});

export const devIp = dev.publicIp;
export const stagingIp = staging.publicIp;

Zastanów się: czy w Terraform nie kopiowałeś już modułów lub plików, które różniły się wyłącznie nazwą i kilkoma parametrami? W Pulumi możesz zastosować dobrze znane ci techniki z programowania, żeby ten problem rozwiązać czyściej.

Bezpośrednie porównanie: prosty scenariusz w trzech narzędziach

Załóżmy spójny scenariusz, który już częściowo zrealizowałeś:

  • utworzenie publicznej instancji w AWS z otwartymi portami 22 i 80,
  • zainstalowanie Nginx i wystawienie prostej strony,
  • możliwość łatwego odtworzenia środowiska w razie potrzeby.

Pytanie do ciebie: jak lubisz myśleć o tym scenariuszu? Bardziej w kategoriach „zasobów chmurowych”, czy raczej „serwera, na którym ma działać aplikacja”? To ukierunkuje wybór narzędzia.

Ten sam cel w Terraform

W Terraform przykład wygląda następująco (uproszczona wersja, bez pełnego VPC):

provider "aws" {
  region = var.aws_region
}

resource "aws_security_group" "web_sg" {
  name        = "demo-web-sg"
  description = "Allow HTTP and SSH"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami                    = var.ami_id
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  user_data = <<EOF
#!/bin/bash
apt-get update -y
apt-get install -y nginx
echo "<h1>Terraform demo</h1>" > /var/www/html/index.html
systemctl enable nginx
systemctl restart nginx
EOF

  tags = {
    Name = "demo-web"
  }
}

output "web_public_ip" {
  value = aws_instance.web.public_ip
}

Całość jest deklaratywna. Zastanów się: czy odpowiada ci prosty model „stan + plan + apply”, czy potrzebujesz czegoś bardziej elastycznego (np. pętli for, warunków w samym języku)?

Ten sam cel w Ansible

W Ansible nie tworzysz instancji, zakładasz, że już istnieje. Jednocześnie konfiguracja wewnątrz VM jest bardzo przejrzysta. Minimalny playbook (pomijając role i strukturę katalogów):

---
- name: Webserver na istniejącej maszynie
  hosts: web
  become: true

  tasks:
    - name: Zainstaluj Nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: yes

    - name: Ustaw stronę startową
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        content: |
          <h1>Ansible demo</h1>
        owner: www-data
        group: www-data
        mode: "0644"

    - name: Upewnij się, że Nginx działa
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: yes

Jaki jest tu punkt ciężkości? Całość koncentruje się na stanie systemu operacyjnego. Wszystko, co chmurowe, jest „na zewnątrz” – robione wcześniej (ręcznie, Terraformem, Pulumi lub skryptem).

Najczęściej zadawane pytania (FAQ)

Co to jest Infrastructure as Code (IaC) i po co mi to w ogóle?

IaC to sposób opisywania infrastruktury (serwery, sieci, bazy danych, load balancery) w plikach tekstowych zamiast przez ręczne klikanie w konsoli chmury czy SSH. Te pliki są trzymane w Git, przechodzą code review, testy i są automatycznie wdrażane – dokładnie jak aplikacja.

Zapytaj siebie: czy dziś potrafisz odtworzyć środowisko „1:1” wyłącznie na podstawie repozytorium, czy musisz dopytywać ludzi? Jeśli bez „człowieka od serwerów” nic się nie da zrobić, IaC pomaga przenieść infrastrukturę z głów ludzi do kodu, ułatwia powtarzalność, historię zmian i współpracę w zespole.

Terraform, Ansible czy Pulumi – od czego zacząć przy pierwszym projekcie?

Zacznij od pytania: co właściwie chcesz automatyzować? Jeśli głównie tworzysz i zmieniasz zasoby w chmurze (VPC, maszyny, bazy, load balancery), naturalnym wyborem na start będzie Terraform lub Pulumi. Jeśli bardziej boli cię konfiguracja systemów (pakiety, usługi, pliki konfiguracyjne na VM), wtedy sensownie jest zacząć od Ansible.

Dla prostego, pierwszego projektu w chmurze publicznej najczęstszy scenariusz wygląda tak: Terraform lub Pulumi do postawienia infrastruktury + ewentualnie Ansible do konfiguracji wnętrza maszyn. Jeśli dopiero wchodzisz w IaC i nie potrzebujesz języków programowania, Terraform będzie zwykle prostszy na start.

Czym się różni podejście deklaratywne od imperatywnego w IaC?

W podejściu deklaratywnym opisujesz stan końcowy: „chcę mieć 1 VM o takim rozmiarze, z tą siecią i tą bazą danych”. Narzędzie (Terraform, Pulumi) samo wylicza, co utworzyć, zmienić lub usunąć, żeby dojść do tego stanu. Pracujesz bardziej na „co ma być”, a mniej na „jak krok po kroku to zrobić”.

W podejściu imperatywnym opisujesz sekwencję działań: „stwórz VM, zainstaluj pakiet X, skopiuj plik Y, zrestartuj usługę Z”. Ansible jest bliżej tego świata – wciąż deklaruje pożądany stan, ale myślisz w kategoriach zadań i kolejności. Zadaj sobie pytanie: czy łatwiej jest ci myśleć o krokach, czy o końcowym obrazie infrastruktury?

Kiedy lepiej wybrać Terraform, a kiedy Ansible lub Pulumi?

Terraform najlepiej sprawdza się do zarządzania zasobami chmurowymi: sieci, maszyny, bazy, kolejki, role IAM. Jeśli twoim głównym bólem jest „klikanie w konsoli AWS/Azure/GCP” i ręczne tworzenie środowisk, zacznij od niego. Ansible z kolei błyszczy przy konfiguracji systemów i aplikacji na VM – instalacja pakietów, konfiguracja serwisów, deployment na serwery.

Pulumi jest ciekawą opcją, gdy chcesz używać „prawdziwych” języków programowania (TypeScript, Python, Go, C#) do opisywania infrastruktury i liczy się dla ciebie możliwość tworzenia bardziej złożonej logiki w kodzie IaC. Zastanów się: jaki masz dziś stos technologiczny i z jakimi językami zespół czuje się swobodnie?

Jak połączyć IaC z Dockerem i Kubernetesem w praktyce?

Najprościej myśleć o tym warstwami. IaC (Terraform lub Pulumi) zajmuje się warstwą infrastruktury: sieciami, maszynami, klastrami Kubernetes (EKS/AKS/GKE), bazami danych czy load balancerami. Kontenery i manifesty K8s możesz opisywać osobno – np. Helm chartami, ArgoCD, Fluxem albo zwykłymi YAML-ami w Git.

Zapytaj: gdzie dziś tracisz najwięcej czasu? Jeśli na ręcznym tworzeniu klastrów i zasobów wokół Kubernetesa – zacznij od IaC w warstwie poniżej K8s. Jeśli klastry masz już gotowe, IaC możesz używać tylko do części elementów platformy (storage, ingressy), a resztę zostawić narzędziom typowo „kubernetesowym”.

Jak IaC wpisuje się w GitOps i proces CI/CD?

W podejściu GitOps repozytorium Git jest jedynym źródłem prawdy o infrastrukturze. Kod Terraform/Ansible/Pulumi trafia do repo, a każda zmiana idzie przez pull request i review. Pipeline CI/CD uruchamia plan i apply (lub odpowiednik w wybranym narzędziu), dzięki czemu stan chmury odzwierciedla to, co zostało zmergowane do głównej gałęzi.

Jeżeli dziś zdarza się, że ktoś „naprawia” produkcję przez konsolę chmurową, zadaj sobie pytanie: jak często te zmiany znikają przy kolejnym wdrożeniu? W modelu GitOps poprawka zawsze zaczyna się od zmiany w kodzie IaC. To mocno ogranicza nieudokumentowane działania i ułatwia audyty oraz debugowanie.

Jak rozpoznać, że czas wdrożyć IaC w moim zespole?

Sprawdź kilka prostych rzeczy. Czy odtworzenie środowiska testowego „1:1” z produkcji zajęłoby godziny lub dni? Czy wiesz, jakie zmiany w infrastrukturze zaszły w ostatnim miesiącu? Czy konfiguracja żyje głównie w Slacku, w głowach ludzi i starych notatkach, zamiast w Git?

Jeśli większość odpowiedzi to „nie wiem” lub „to byłby dramat”, to dobry moment, żeby zacząć od małego kroku: opisać w IaC jedno środowisko, jedną aplikację lub jeden fragment infrastruktury. Na start wybierz obszar, który dziś najbardziej boli – zbyt długie stawianie środowisk, różnice między dev a prod, czy brak historii zmian – i tam przetestuj wybrane narzędzie.

Kluczowe Wnioski

  • IaC przenosi infrastrukturę z „głowy admina” do repozytorium Git: konfiguracja staje się kodem, który można wersjonować, recenzować, testować i wdrażać tak samo jak aplikację – pytanie brzmi: czy dziś naprawdę wiesz, co zmieniło się w twojej infrastrukturze w ostatnim miesiącu?
  • Największe zyski z IaC to powtarzalność i odtwarzalność środowisk: od dev po prod możesz odtworzyć całą infrastrukturę jedną komendą, zamiast odgrzebywać stare checklisty i historię kliknięć w panelu chmurowym.
  • IaC porządkuje proces zmian: każda modyfikacja to commit i pull request, z pełnym diffem i informacją kto, kiedy i dlaczego coś zmienił – jeśli dziś prod różni się od test „bo ktoś coś poprawił na szybko”, to właśnie tu masz największą dziurę.
  • Różnica między podejściem deklaratywnym (Terraform, Pulumi) a bardziej imperatywnym (Ansible) jest kluczowa: najpierw odpowiedz sobie, czy wolisz opisywać stan końcowy („co ma być”), czy sekwencję kroków („jak to osiągnąć”), a dopiero potem dobieraj narzędzie.
  • Wybór narzędzia zależy od aktualnego bólu: jeśli problemem jest szybkie i bezpieczne tworzenie zasobów w chmurze, naturalnym kierunkiem jest Terraform lub Pulumi; jeśli głównie męczy cię konfiguracja systemów i aplikacji, zacznij od Ansible – co dziś zabiera ci więcej czasu?
  • IaC ułatwia eksperymenty i sprzątanie po nich: możesz stworzyć osobny branch lub stack, odpalić alternatywną wersję infrastruktury, a gdy test nie wypali, usuwasz wszystko jednym poleceniem zamiast tygodniowego „odkręcania” zmian na serwerach.