Infrastructure as Code – po co męczyć się z kodem zamiast klikać
Infrastructure as Code (IaC) to sposób opisywania całej infrastruktury w postaci kodu, który można wersjonować, testować i automatyzować. Zamiast polegać na pojedynczej osobie „która wie, gdzie co jest w klikanym panelu”, zespół dostaje czytelny, powtarzalny opis zasobów. Znika efekt „magii w konsoli”, rośnie przewidywalność, a bus factor (ile osób może zniknąć z projektu, zanim wszystko się posypie) spada do akceptowalnego poziomu.
Klikanie vs deklaratywny opis infrastruktury
Klikane środowiska w panelach chmurowych czy w vCenter mają jeden wspólny problem: trudno je odtworzyć. Da się, ale tylko wtedy, gdy:
- osoba, która je klikała, nadal jest w firmie,
- pamięta co zrobiła i dlaczego,
- zapisano gdzieś zrzuty ekranu lub ręczne notatki,
- nikt po drodze nie „podrasował” konfiguracji ręcznie na produkcji.
W podejściu deklaratywnym (Terraform, częściowo Ansible) opisujesz stan docelowy: jak ma wyglądać sieć, ile ma być maszyn, jaką mają mieć konfigurację. Narzędzie samo wylicza, co trzeba zmienić. Zamiast instrukcji „wejdź w panel, kliknij tu i tu”, trzymasz pliki .tf lub .yml w repozytorium Git i zmiany infrastruktury przechodzą przez pull requesty jak normalny kod.
Koszt ręcznego zarządzania infrastrukturą vs koszt wejścia w IaC
Dla małego zespołu wejście w IaC to koszt nauki i początkowej konfiguracji. Trzeba:
- opanować podstawy Terraform i Ansible,
- ustawić backend na stan Terraform,
- zorganizować proste repozytorium Git,
- dogadać minimalne zasady code review.
Ten koszt jest jednorazowy, a zwraca się przy każdym kolejnym środowisku, każdym nowym projekcie i każdym odtworzeniu zasobów po awarii. Koszt klikanej infrastruktury jest odwrotny: na początku jest tanio i szybko, później rośnie lawinowo, gdy pojawia się więcej środowisk, więcej ludzi i więcej ręcznych wyjątków. Prawdziwy rachunek przychodzi wtedy, gdy trzeba odtworzyć produkcję na DR albo w nowym regionie – bez IaC to często tygodnie żmudnego odtwarzania „z pamięci”.
Gdzie IaC robi największą różnicę
Największy efekt „efekt vs wysiłek” przy Infrastructure as Code widać tam, gdzie infrastruktura się powtarza lub często zmienia:
- Środowiska testowe i feature branches – możliwość podnoszenia całego stosu na krótko, a potem niszczenia go jednym poleceniem
terraform destroy. - Staging / pre-prod – szybkie odtworzenie produkcji w pomniejszonej skali, bez ręcznego przeklikiwania każdego zasobu.
- Disaster Recovery – scenariusze DR przestają być teorią; da się naprawdę postawić środowisko w innym regionie chmury, bo jego definicja jest w kodzie.
- Scale-out – kiedy ruch rośnie, dodanie kolejnych instancji czy usług staje się kwestią zmiany kilku wartości w pliku i ponownego
apply.
Ręczna zmiana vs zmiana w kodzie – krótki, praktyczny przykład
Załóżmy, że trzeba dodać nową maszynę aplikacyjną w AWS.
Ścieżka „klikana”:
- Wejście do AWS Console.
- Przejście do EC2, kliknięcie „Launch Instance”.
- Wybór AMI, typu instancji, VPC, subnetu, security group.
- Dodanie tagów, ustawienie klucza SSH.
- Zapisanie gdzieś informacji „jak to było zrobione”, o ile ktokolwiek ma na to czas.
Ścieżka z Terraform:
- Dodanie nowego resource w pliku
app-instances.tfz identycznymi parametrami jak poprzednie, różniąc np. nazwą. - Wykonanie
terraform plan, by sprawdzić, czy wszystko jest spójne. - Review w pull requeście przez drugą osobę.
terraform apply– instancja powstaje, opis zostaje na zawsze w repozytorium.
Dochodzi trochę „papierologii” (kod, review), ale koszt każdej kolejnej zmiany spada. Dla zespołu 2–5 osób to często jedyna realna szansa, żeby się nie zajechać ręcznymi poprawkami po nocach.
Terraform i Ansible – role, różnice i sensowne zastosowania
Terraform i Ansible często występują razem, ale pełnią inne role. Terraform zarządza zasobami infrastruktury, a Ansible zajmuje się konfiguracją i deploymentem na tych zasobach. Używanie ich zamiennie zwykle kończy się albo przerostem formy, albo niepotrzebnym komplikowaniem prostych rzeczy.
Terraform – zarządzanie infrastrukturą jako całością
Terraform jest deklaratywnym narzędziem do zarządzania infrastrukturą: od maszyn wirtualnych, przez sieci, po usługi PaaS typu bazy danych w chmurze. Wpisujesz w kodzie, że ma istnieć VPC, trzy subnety, dwie instancje, load balancer i baza danych – a Terraform oblicza różnicę między stanem bieżącym a docelowym i wykonuje plan zmian.
Typowe obszary, gdzie Terraform się sprawdza:
- tworzenie sieci (VPC, VNets, VPC Networks),
- tworzenie i skalowanie maszyn wirtualnych lub grup autoscale,
- zarządzanie bazami danych jako usługą (RDS, Cloud SQL, Azure Database),
- usługi dodatkowe: kolejki, topic’i, DNS, certyfikaty.
Terraform trzyma stan infrastruktury w pliku terraform.tfstate, co umożliwia obliczanie, co dokładnie trzeba zmienić przy każdym kolejnym uruchomieniu. Dzięki temu zmiany są przewidywalne i powtarzalne.
Ansible – konfiguracja systemów i deployment aplikacji
Ansible działa inaczej: opisuje zadania, które mają zostać wykonane na hostach. To narzędzie bardziej proceduralne (task-based), ale moduły Ansible starają się być idempotentne – wielokrotne uruchomienie tego samego playbooka nie powinno wprowadzać zbędnych zmian.
Ansible sprawdza się szczególnie w:
- instalacji pakietów i konfiguracji systemu (Linux, Windows),
- konfiguracji serwerów aplikacyjnych, reverse proxy (nginx, Apache),
- wdrażaniu aplikacji (pull/push buildów, kopiowanie artefaktów),
- konfiguracji użytkowników, usług, crontabów.
W przeciwieństwie do Terraform, Ansible nie trzyma centralnego „stanu” środowiska, tylko wykonuje zadania bazując na aktualnej konfiguracji hostów.
Deklaratywność Terraform vs zadaniowe podejście Ansible
Kluczowa różnica dla projektowania IaC:
- Terraform – definicja co ma istnieć: zasoby, ich parametry, relacje. Narzędzie samo planuje zmiany.
- Ansible – definicja co zrobić: zainstalować pakiet, skopiować plik, uruchomić usługę, zrestartować serwis.
To nie znaczy, że w Ansible nie da się pisać deklaratywnie. Moduły typu ansible.builtin.user, file, service mają parametry state=present/absent, więc zachowują się dość deklaratywnie. Jednak cała logika przepływu, warunki, zależności to nadal playbook i role – kod zadaniowy.
Typowy podział pracy: Terraform stawia, Ansible konfiguruje
Przy budżetowym podejściu do DevOps sensowny schemat najczęściej wygląda tak:
- Terraform:
- tworzy sieć,
- tworzy bazy danych jako usługę,
- tworzy maszyny wirtualne lub instancje w chmurze,
- tworzy load balancer, DNS, certyfikaty.
- Ansible:
- konfiguruje system operacyjny na maszynach,
- instaluje docker / runtime aplikacji,
- wdraża same aplikacje,
- konfiguruje monitoring, logowanie.
Taki podział ułatwia utrzymanie: gdy trzeba dodać nową instancję, Terraform ją stawia, a Ansible skutecznie dociąga konfigurację do oczekiwanego stanu. Nikt nie zastanawia się, czy na tej konkretnej maszynie ktoś ręcznie nie zmienił konfiguracji.
Przykładowy przepływ: od Terraform do Ansible
Prosty przepływ może wyglądać tak:
- W repo „infra” tworzysz w Terraform zasoby: VPC, subnety, security groups, dwie instancje EC2.
- Terraform tworzy też
outputz adresami IP nowych maszyn. - Te adresy IP trafiają do inventory Ansible (ręcznie, generacją pliku lub dynamicznym inventory).
- Uruchamiasz playbook Ansible, który:
- instaluje nginx,
- kopiuje plik konfiguracyjny,
- deployuje aplikację,
- uruchamia usługę i włącza ją w systemd.
Terraform i Ansible można też połączyć w pipeline CI/CD, ale dla małego zespołu równie skuteczny jest półautomatyczny wariant: przygotowany skrypt, który po terraform apply uruchamia odpowiedni playbook.
Podstawy Terraform w praktyce: od pierwszego pliku do prostych modułów
Terraform ma niski próg wejścia, jeśli zacznie się od minimalnego, ale kompletnego projektu. Nie ma sensu tworzyć na start kilkunastu katalogów i wspólnej „platformy”. Lepiej zacząć od jednej aplikacji lub jednego środowiska i stopniowo wydzielać moduły.
Minimalna struktura projektu Terraform
Dla małego projektu wystarczy na początek struktura:
main.tf– główna definicja zasobów i providerów,variables.tf– definicje zmiennych wejściowych,outputs.tf– definicje wartości wyjściowych,terraform.tfvars– wartości zmiennych dla konkretnego środowiska (niekoniecznie commitowane do Gita).
Przykładowy fragment main.tf dla AWS:
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
Takie minimum pozwala już zarządzać zasobami. W kolejnym kroku można z czasem wydzielać poszczególne grupy zasobów do osobnych plików, np. network.tf, compute.tf, database.tf. Nie ma potrzeby rozdmuchiwania struktury tylko po to, żeby wyglądała „enterprise”.
Providerzy i ich wersjonowanie
Provider to plugin Terraform do konkretnego dostawcy: AWS, Azure, GCP, VMware, DNS, monitoring itp. Z punktu widzenia małego zespołu ważne są dwie rzeczy:
- zablokowanie wersji providera (żeby update nie zepsuł istniejącej konfiguracji),
- unikanie przypadkowego mieszania za wielu providerów na raz w jednym projekcie.
W terraform bloku warto ustawić wersję:
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
Dla środowiska produkcyjnego bezpiecznie jest aktualizować providery świadomie co jakiś czas, a nie „przy okazji”. Dobrą praktyką jest wykonywanie terraform plan po aktualizacji providera i sprawdzenie, czy nie próbuje on wprowadzić nieplanowanych zmian na istniejących zasobach.
Deklaracja prostych zasobów – przykład sieci i instancji
Prosty przykład sieci VPC i instancji w AWS:
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-public-subnet"
}
}
resource "aws_instance" "app" {
ami = var.app_ami
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
tags = {
Name = "demo-app-1"
}
}
Tak zdefiniowane zasoby są w pełni reproducowalne. Zmiana nazwy, typu instancji czy dodanie drugiej instancji to kwestia prostych modyfikacji w kodzie. Terraform sam rozpozna, czy zasób wymaga zastąpienia, czy może być zmodyfikowany w miejscu.
Plan i apply – dlaczego terraform plan to obowiązek
Terraform oferuje dwa kluczowe polecenia:
terraform plan– pokazuje, co zostanie zmienione, dodane lub usunięte,terraform apply– faktycznie wykonuje zmiany.
Praca ze zmiennymi i środowiskami bez przerostu formy
Przy pierwszych projektach najlepiej ograniczyć się do prostego podziału na środowiska oparty o pliki *.tfvars i kilka zmiennych sterujących. Zamiast od razu tworzyć wielopoziomowy system workspace’ów i osobnych repozytoriów na każde środowisko, można zacząć od:
terraform.tfvars– środowisko domyślne (np. dev),dev.tfvars,stage.tfvars,prod.tfvars– warianty dla poszczególnych środowisk.
Uruchomienie z konkretnym zestawem wartości:
terraform plan -var-file="stage.tfvars"
terraform apply -var-file="stage.tfvars"
W zmiennych trzymasz różnice: region, klasy maszyn, rozmiary baz, nazwy prefixów. To wystarcza, żeby jedno źródło Terraform obsługiwało kilka środowisk bez rozjeżdżania się konfiguracji.
Przykładowa definicja zmiennej i użycie:
variable "instance_type" {
type = string
description = "Typ instancji dla aplikacji"
default = "t3.micro"
}
resource "aws_instance" "app" {
ami = var.app_ami
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
}
W prod.tfvars można wtedy nadpisać tylko jedną wartość:
instance_type = "t3.medium"
Bez kopiowania całych plików i bez ręcznej edycji kodu pod środowisko.
Local values i proste etykietowanie zasobów
Dobrym kompromisem między prostotą a porządkiem jest użycie lokalnych wartości (locals) do nazewnictwa i tagowania. Nie trzeba budować całego frameworka do tagów – wystarczy mały blok:
locals {
project = "demo-app"
env = var.environment
common_tags = {
Project = local.project
Environment = local.env
ManagedBy = "terraform"
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = merge(
local.common_tags,
{
Name = "${local.project}-${local.env}-vpc"
}
)
}
Dzięki temu zmiana nazwy projektu albo skrótów środowisk nie wymaga edycji w dziesiątkach miejsc. Dla małego zespołu to mały wysiłek, a oszczędza sporo czasu przy przeglądaniu rachunków i szukaniu „co do kogo należy”.
Od powtarzalnego kodu do pierwszego modułu
Moment na moduły pojawia się wtedy, gdy kopiujesz ten sam blok kodu trzeci raz. Przy dwóch powtórzeniach zwykle jeszcze łatwiej to znieść niż budować abstrakcję, którą trzeba zrozumieć i utrzymać.
Typowy przykład: kilka podobnych instancji lub application stacków. Najpierw kod bywa skopiowany:
resource "aws_instance" "app1" {
ami = var.app_ami
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
tags = {
Name = "app1"
}
}
resource "aws_instance" "app2" {
ami = var.app_ami
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
tags = {
Name = "app2"
}
}
Jeśli takich fragmentów jest więcej, można wydzielić moduł w katalogu modules/app_instance:
# modules/app_instance/main.tf
resource "aws_instance" "this" {
ami = var.ami
instance_type = var.instance_type
subnet_id = var.subnet_id
tags = merge(
var.tags,
{
Name = var.name
}
)
}
# modules/app_instance/variables.tf
variable "ami" {
type = string
}
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "subnet_id" {
type = string
}
variable "name" {
type = string
}
variable "tags" {
type = map(string)
default = {}
}
Użycie modułu w głównym projekcie:
module "app1" {
source = "./modules/app_instance"
ami = var.app_ami
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
name = "app1"
tags = local.common_tags
}
module "app2" {
source = "./modules/app_instance"
ami = var.app_ami
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
name = "app2"
tags = local.common_tags
}
Na starcie to często jedyny moduł, jaki jest naprawdę potrzebny. Z czasem można dołożyć moduł sieciowy czy moduł bazy, ale dopiero gdy faktycznie coś zaczyna być wielokrotnie używane.
Moduły z Registry – kiedy użyć gotowca, a kiedy to przerost formy
Publiczne Terraform Registry oferuje masę modułów. Dla małego projektu przydają się głównie tam, gdzie konfiguracja jest rozbudowana i mało przyjemna, np. sieć w AWS czy konfiguracja S3 z wersjonowaniem i politykami.
Dwa sensowne, budżetowe scenariusze:
- zastosowanie gotowego modułu do VPC, żeby nie przepisywać 100 linii konfigurowania routingu i NAT-ów,
- skorzystanie z modułu do S3 / Azure Storage / GCS z sensownymi domyślnymi politykami.
Przykład użycia gotowego modułu VPC:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "${local.project}-${local.env}"
cidr = "10.0.0.0/16"
azs = ["eu-central-1a", "eu-central-1b"]
public_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]
enable_nat_gateway = false
single_nat_gateway = false
tags = local.common_tags
}
Tu da się realnie zaoszczędzić czas i zmniejszyć liczbę potencjalnych błędów. Z drugiej strony, moduł do stworzenia pojedynczej instancji z dwoma tagami zwykle tylko wprowadza zamieszanie. Korzyść z „ponownego użycia” jest iluzoryczna, a debugowanie – trudniejsze.
Praktyczny workflow Git + Terraform dla małego zespołu
Bez rozbudowanego CI wystarczy prosty, ale konsekwentny sposób pracy:
- nowy branch na zmianę infrastruktury,
- lokalne
terraform plan -var-file="dev.tfvars"i dołączenie planu (np. jako załączonego logu) do PR, - review: ktoś inny patrzy przede wszystkim na to, czy plan nie usuwa czegoś niespodziewanego,
- merge + manualne
terraform applywykonywane przez osobę „od infra”.
Taki proces nie wymaga od razu Jenkinsów i GitLabów, a i tak ogranicza ryzyko „kliknięcia z rozpędu” czegoś groźnego. Plan jest zawsze jawny, a commity odpowiadają temu, co dzieje się w chmurze.

Podstawy Ansible w praktyce: od jednorazowego playbooka do ról
Minimalna struktura małego projektu Ansible
Na początek wystarczy prosty układ katalogów:
inventory/– pliki z listą hostów (np.dev,prod),playbooks/– główne playbooki,group_vars/ihost_vars/– opcjonalnie, na później.
Przykładowy inventory/dev:
[app]
10.0.1.10
10.0.1.11
[db]
10.0.11.10
Najprostszy playbook instalujący nginx i prosty plik konfiguracyjny:
# playbooks/web.yml
- name: Configure web servers
hosts: app
become: true
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Deploy nginx config
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: "0644"
notify:
- restart nginx
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Uruchomienie:
ansible-playbook -i inventory/dev playbooks/web.yml
To w zupełności wystarczy, żeby zautomatyzować konfigurację kilku serwerów bez tworzenia ról, kolekcji i pełnego frameworka. Dla wielu małych projektów taki „płaski” playbook żyje zaskakująco długo.
Jednorazowy playbook jako lepszy „skrypt bash”
Zamiast kolejnego skryptu bash typu setup.sh z apt install i kopiowaniem plików można napisać pojedynczy playbook, którego da się powtarzalnie uruchamiać. Różnica jest taka, że:
- moduły Ansible są idempotentne – nie instalują w kółko tego samego,
- łatwiej dokładać kolejne zadania,
- masz naturalny podział na zadania i handlery (np. restart usług).
Przykładowo, zamiast:
#!/bin/bash
apt update
apt install -y nginx
cp nginx.conf /etc/nginx/nginx.conf
systemctl restart nginx
powstaje minimalny playbook, a do tego prosty template Jinja2: templates/nginx.conf.j2, gdzie można wstrzykiwać zmienne środowiskowe.
Inventory dynamiczne z Terraform – prosty most między światem IaC
Łączenie Terraform i Ansible nie wymaga wyrafinowanych narzędzi. Wystarczy wygenerować stan hostów w formacie, który Ansible potrafi czytać. Dla małego zespołu sprawdzają się dwa podejścia.
Podejście pierwsze – generowanie pliku INI/INI-like przez prosty skrypt shell/python, który korzysta z terraform output -json:
terraform output -json app_ips > app_ips.json
Na tej podstawie skrypt tworzy inventory/dev. To rozwiązanie „na szybko”, ale działa przewidywalnie i jest łatwe do zrozumienia.
Podejście drugie – inventory dynamiczne. Ansible ma wbudowane pluginy do AWS, Azure, GCP, dzięki którym można odpytywać API dostawcy. W małym projekcie często wystarczy jednak prostszy trick: tagujesz wszystkie instancje Terraformem:
tags = merge(
local.common_tags,
{
Role = "app"
}
)
i konfigurujesz plugin AWS EC2 inventory, żeby zbierał hosty po tagu Role=app. Nie trzeba wtedy ręcznie aktualizować inventory po każdej zmianie w Terraform.
Od playbooka do roli – kiedy przejść na wyższy poziom
Rola ma sens, kiedy:
- te same zadania chcesz użyć na wielu projektach lub środowiskach,
- playbook zaczyna mieć 200+ linii i ciężko się go czyta,
- wiele osób modyfikuje te same bloki konfiguracji.
Najbardziej naturalny kandydat to rola systemowa, np. common, która robi porządki: ustawia strefę czasową, logrotate, kilka pakietów diagnostycznych, userów z kluczami SSH.
Struktura takiej roli (w roles/common):
roles/
common/
tasks/
main.yml
vars/
main.yml
handlers/
main.yml
Przykładowy roles/common/tasks/main.yml:
- name: Install base packages
ansible.builtin.package:
name:
- htop
- curl
- vim
state: present
- name: Ensure timezone is set
ansible.builtin.file:
src: "/usr/share/zoneinfo/{{ common_timezone }}"
dest: /etc/localtime
state: link
Playbook korzystający z roli jest prosty:
- name: Base system config
hosts: all
become: true
vars:
common_timezone: "Europe/Warsaw"
roles:
- common
Od tej chwili wszystkie serwery przechodzą przez ten sam zestaw ustawień. Dodanie nowego pakietu czy zmiana strefy czasowej to jedno miejsce w repozytorium.
Zmienne grupowe i hostowe – bez overengineeringu
Zamiast rozlewać zmienne po playbookach, wygodniej trzymać je w group_vars i host_vars. Nie trzeba od razu wykorzystywać całego systemu grup – wystarczy prosty podział: group_vars/all i group_vars/app.
# group_vars/all.yml
ansible_user: ubuntu
ansible_ssh_common_args: "-o StrictHostKeyChecking=no"
# group_vars/app.yml
app_env: "dev"
app_port: 8080
Dzięki temu playbooki są bardziej uniwersalne, a różnice środowiskowe nie rozlewają się po dziesięciu plikach. Kiedy pojawi się produkcja, można po prostu dodać group_vars/prod.yml lub osobny inventory z innymi wartościami.
Ansible a koszty – co realnie pomaga w budżecie
Automatyzacja konfiguracji ma wpływ na rachunki w dwóch miejscach:
- czas ludzi – mniej ręcznych zmian, szybkie odtwarzanie serwerów,
- koszty infrastruktury – łatwiej konsekwentnie wdrożyć oszczędne ustawienia.
Przykładowo, jeżeli w roli common zdefiniujesz:
- konfigurację logrotate, żeby logi nie zjadały dysku,
- domyślne limity dla serwerów HTTP,
Automatyczne gaszenie i podnoszenie środowisk
Najprostsza oszczędność, którą da się zautomatyzować Ansible, to po prostu wyłączanie serwerów poza godzinami pracy. Jeżeli dev czy test stoją na zwykłych VM-kach, prosty playbook typu „stop non-prod” potrafi ściąć rachunek o kilkadziesiąt procent, bez kombinowania z autoscalingiem.
Przykładowy schemat:
- Terraform tworzy instancje z tagiem
Env=dev, - plugin inventory (np. AWS EC2) wrzuca je do grupy
dev, - playbook
ops/stop-dev.ymlzatrzymuje maszyny.
- name: Stop non-prod instances
hosts: dev
gather_facts: false
tasks:
- name: Stop instance (AWS EC2)
community.aws.ec2:
instance_ids: "{{ ec2_instance_id }}"
state: stopped
Taki playbook można podpiąć pod najtańszy możliwy scheduler: cron na małym serwerze, job w GitLab CI, nawet Github Actions z darmowym limitem. Nie trzeba od razu ustawiać pełnego „sustainability program” – kilka linijek YAML daje realną różnicę na fakturze.
Świadome używanie ról z Galaxy i gotowców
Gotowe role z Ansible Galaxy kuszą tym, że „robią wszystko”: instalacja Postgresa, Nginx + Let’s Encrypt, monitoring. Problem pojawia się wtedy, gdy:
- rola wnosi 150 zmiennych, z których używasz trzech,
- aktualizacja roli zmienia defaulty i rozwala istniejącą konfigurację,
- debugowanie wymaga grzebania godzinę w strukturze kogoś innego.
Praktyczne podejście dla małego zespołu:
- Użyć roli z Galaxy jako „szkieletu”: wziąć kilka zadań, przerobić je pod siebie, wyrzucić 80% niepotrzebnych opcji.
- Unikać automatycznych update’ów roli – traktować ją jak część własnego kodu, wersjonować w repo.
- Dla usług krytycznych (baza, reverse proxy) pisać role samodzielne, ale proste, pokrywające 100% potrzeb zamiast 20% „możliwości świata”.
Zwykle taniej wychodzi mieć rolę z 15 zadaniami, które każdy rozumie, niż 200-zadaniowego potwora, którego boi się dotknąć nowa osoba w zespole.
Projektowanie IaC pod kątem kosztów, prostoty i małego zespołu
„Good enough” zamiast idealnej architektury
Większość poradników IaC pokazuje rozwiązania projektowane pod duże organizacje: osobne konta chmurowe na każdy system, wielopoziomowy podział na organizacje, kilkanaście warstw modułów. W kilkuosobowym zespole taki model często generuje więcej kosztu (czas + błędy) niż pożytku.
Dla małego projektu sprawdza się skromniejszy, ale przejrzysty układ:
- jeden tenant / konto chmurowe na firmę + osobne projects/resource groups na systemy,
- podział Terraform na kilka logicznych „stacków” (np.
network,app,data), a nie kilkadziesiąt mini-modułów, - prosty naming konwencji typu
<project>-<env>-<component>.
W praktyce bardziej opłaca się przepłacić godzinę miesięcznie na ręczne przejrzenie billingów niż tydzień na dopinanie stacku organizacyjnego klasy enterprise.
Gdzie IaC się opłaca, a gdzie można zostać przy „klikaniu”
Nie wszystkie elementy infrastruktury generują taki sam zwrot z automatyzacji. Dobrze jest rozdzielić to, co ciągle się zmienia, od tego co praktycznie nigdy nie rusza.
Za pomocą Terraform/Ansible opłaca się opisać:
- podstawową sieć (VPC, subnety, routing, security groups),
- maszyny aplikacyjne, load balancery, bazy w modelu „DBaaS”,
- konfigurację serwerów, która bardzo boli, gdy trzeba ją odtwarzać z pamięci.
Elementy typu:
- pojedyncze konta użytkowników w panelu billingowym,
- jeden ręcznie skonfigurowany alarm na użycie budżetu,
- opcjonalne integracje z marketplace’ów,
często szybciej i taniej załatwić kliknięciem, byle raz, opisując decyzję w README. Próba „zkodowania” każdego przełącznika zabiera czas, który lepiej przeznaczyć na automatyzację tego, co faktycznie dotykasz co tygodnia.
Jedno repo czy wiele? Aspekt kosztowy i organizacyjny
Podstawowe pytanie: trzymać Terraform i Ansible w jednym repo czy rozdzielić? Z perspektywy małego zespołu dominują dwa czynniki – koszt czasu i prostota on-boardingu.
Model 1 – jedno repo infra:
- łatwiej zrozumieć całość: katalog
terraform/, katalogansible/, jedno README, - prostszy onboarding nowych osób; jeden zestaw narzędzi, jedna historia zmian,
- większa szansa na spójne review całej ścieżki: od sieci, przez serwery, po konfigurację.
Model 2 – osobne repo terraform-<project>, ansible-<project>:
- często potrzebny dopiero, gdy zespół rośnie i pojawiają się osobne „ekipy” od infra i configu,
- dokłada narzut na CI/CD i procesy PR (więcej pipeline’ów, więcej integracji),
- ułatwia separację uprawnień, ale to rzadko jest problem w 3–5 osobowej grupie.
W praktyce sensowne jest zacząć od jednego repo. Rozdział bywa opłacalny dopiero wtedy, gdy realnie przeszkadza liczba zmian w historii lub konflikt priorytetów między zespołami.
Envy, czyli prod jak dev, ale jednak tańszy
Naturalny odruch to: „zróbmy dev jak prod, tylko mniejszy”. W IaC łatwo to osiągnąć przez zmienne – te same moduły obsługują różne rozmiary instancji, liczbę replik itd. Kluczowe jest utrzymanie jednego źródła prawdy, zamiast dwóch rozjeżdżających się światów.
Przykładowe podejście z locals i tfvars:
# variables.tf
variable "env" {
type = string
default = "dev"
}
variable "instance_type" {
type = string
}
# locals.tf
locals {
name_prefix = "${var.project}-${var.env}"
}
W plikach dev.tfvars i prod.tfvars różni się jedynie rozmiar i liczba instancji:
# dev.tfvars
env = "dev"
instance_type = "t3.small"
app_count = 1
# prod.tfvars
env = "prod"
instance_type = "t3.large"
app_count = 3
Dzięki temu można zachować identyczny schemat sieci, te same security groups i konfiguracje, a równocześnie nie przepalać pieniędzy na duże maszyny w środowiskach, które służą głównie developerom.
Standardy tagowania i naming – prosta rzecz, która ratuje rachunki
Porządny system tagowania brzmi jak biurokracja, ale w małym zespole realnie ułatwia kontrolę kosztów. Gdy każda maszyna, bucket czy baza ma czytelne tagi, billing z chmury zamienia się z „losowej listy zasobów” w zrozumiały raport.
Minimalny zestaw tagów, który da się łatwo wprowadzić Terraformem:
Project– nazwa systemu lub klienta,Env–dev,staging,prod,Owner– zespół lub osoba odpowiedzialna,CostCenter– jeżeli firma tego wymaga.
Wystarczy raz zdefiniować to w locals:
locals {
common_tags = {
Project = var.project
Env = var.env
Owner = var.owner
ManagedBy = "terraform"
}
}
i później konsekwentnie robić:
resource "aws_instance" "app" {
# ...
tags = merge(
local.common_tags,
{ Role = "app" }
)
}
Kiedy po pół roku ktoś pyta „co możemy wyłączyć, bo za drogo?”, filtr w konsoli po Env=dev i Owner=team-x daje szybkie odpowiedzi. Bez tagów zwykle kończy się na strzelaniu „chybił trafił” po dziwnie nazwanych maszynach.
Zarządzanie stanem w Terraform bez przepalania budżetu
Remote state – najprostsza konfiguracja, która ma sens
Plik stanu Terraform (terraform.tfstate) to serce infrastruktury jako kodu. Trzymanie go lokalnie na laptopie jest wygodne na start, ale przy więcej niż jednej osobie w zespole szybko robi się niebezpieczne. Na szczęście bez dużych wydatków da się ustawić zdalne przechowywanie stanu.
Minimalna, tania konfiguracja dla popularnych dostawców:
- AWS – bucket S3 + tabela DynamoDB do locków,
- Azure – Storage Account (blob) + kontener na pliki stanu,
- GCP – bucket w Cloud Storage.
Przykład dla AWS:
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "project-a/prod/terraform.tfstate"
region = "eu-central-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
Koszt: kilka groszy miesięcznie za przechowywanie małych plików i tabelę locków. Zysk: brak sytuacji, w której dwie osoby jednocześnie zmieniają ten sam zasób i nadpisują sobie stan.
Podział stanu na „stacki” zamiast jednego wielkiego pliku
Jednym z częstszych antywzorców jest trzymanie całej infrastruktury w jednym terraform.tfstate: sieć, bazy, maszyny, S3, wszystko. Na początku wygodne, ale potem:
- każde
plantrwa wieki, - konflikty zespołowe mnożą się, bo każdy dotyka tego samego stanu,
- ryzyko, że mała zmiana dotknie zasoby produkcyjne, rośnie z każdym nowym modułem.
Zamiast tego lepiej wydzielić kilka osobnych katalogów/stacków, każdy ze swoim backendem:
network/– VPC, subnety, routing, peering,data/– bazy, kolejkowanie, cache,app/– instancje, ASG, load balancery, DNS.
Każdy z nich ma własny plik backend.tf wskazujący na inny klucz w tym samym bucketcie:
# network/backend.tf
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "project-a/network/terraform.tfstate"
region = "eu-central-1"
}
}
To niewielki narzut konfiguracyjny, a w zamian można spokojnie zaktualizować np. security groupy bez obawy, że przypadkowo dotknie się konfiguracji RDS-a.
Udostępnianie stanu między stackami – minimum komplikacji
Gdy infrastruktura zostanie rozbita na kilka stacków, pojawia się potrzeba przekazywania danych między nimi: VPC ID z network do app, endpoint bazy do modułu aplikacyjnego itd. Terraform ma na to mechanizm terraform_remote_state, który da się używać w miarę prosto, byle nie przesadzić z poziomami zależności.
Przykład: stack network eksportuje VPC ID i subnety:
# network/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
Stack app odczytuje te dane:
# app/network.tf
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "mycompany-terraform-state"
key = "project-a/network/terraform.tfstate"
region = "eu-central-1"
}
}
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
# ...
}
Dobrze jest ograniczyć liczbę takich zależności do kilku kluczowych punktów. Gdy każdy stack zaczyna czytać po pięć innych, dochodzi do „pajęczyny”, którą trudno ogarnąć nowej osobie w zespole.
Bezpieczeństwo stanu – najprostsze praktyki, które wystarczą
Stan Terraform może zawierać wrażliwe dane: hasła do baz, klucze, sekrety generowane przez dostawcę. Nie ma sensu inwestować w zaawansowane systemy tajemnic, jeśli na co dzień i tak zapisujesz je w prostych zmiennych. Kilka podstawowych kroków robi różnicę:
- zawsze włączone szyfrowanie na buckecie/Storage Account,
- ograniczone uprawnienia: tylko osoby „od infra” mają dostęp do bucketu ze stanem,
- wyłączenie logowania planów z pełnymi wartościami sekretnych zmiennych w CI.
Dodatkowo, zmienne typu hasła czy klucze API lepiej przekazywać jako sensitive i przez -var lub TF_VAR_* w środowisku, zamiast trzymać je w repo:
Najczęściej zadawane pytania (FAQ)
Co to jest Infrastructure as Code i po co mi to w małym zespole?
Infrastructure as Code (IaC) to sposób opisywania infrastruktury (maszyny, sieci, bazy, load balancery) w postaci kodu, zamiast klikania wszystkiego w panelach chmurowych. Pliki z definicjami trafiają do Gita, więc można je wersjonować, robić code review, testować zmiany i łatwo odtwarzać środowiska.
Nawet w małym zespole zyskujesz na przewidywalności i mniejszej zależności od „tej jednej osoby od AWS-a”. Raz płacisz koszt wejścia (nauka narzędzi, podstawowe repo, minimalny proces review), a później każde nowe środowisko, DR czy migracja to głównie kopiowanie i modyfikacja istniejącego kodu, zamiast tygodni klikania i odtwarzania z pamięci.
Jaka jest różnica między Terraform a Ansible i kiedy używać którego?
Terraform opisuje, jakie zasoby mają istnieć: sieci, instancje, bazy danych, load balancery, DNS, certyfikaty. Działa deklaratywnie – podajesz stan docelowy, a narzędzie samo liczy, co trzeba dodać, usunąć lub zmienić. Przechowuje stan w pliku terraform.tfstate, dzięki czemu kolejne uruchomienia są przewidywalne.
Ansible skupia się na tym, co zrobić na już istniejących hostach: zainstalować pakiety, skonfigurować system, zdeployować aplikację, ustawić crony, monitoring. Ma podejście zadaniowe (task-based), choć wiele modułów działa idempotentnie – wielokrotne uruchomienie playbooka nie „rozjeżdża” konfiguracji.
Praktyczny podział pracy jest prosty: Terraform stawia infrastrukturę (maszyny, sieć, usługi PaaS), a Ansible ją konfiguruje (system, runtime, aplikacje). Mieszanie ról zwykle kończy się niepotrzebnym skomplikowaniem całości.
Czy Terraform i Ansible można używać razem w jednym procesie CI/CD?
Tak, to wręcz najrozsądniejszy wariant w większości projektów. Typowy pipeline wygląda tak: najpierw Terraform tworzy lub aktualizuje zasoby (np. nowe instancje EC2 i load balancer), zapisuje ich adresy IP w outputach, a następnie te adresy trafiają do inventory Ansible (statycznie lub dynamicznie). Kolejny krok uruchamia playbooki Ansible, które dogrywają konfigurację i aplikacje.
Taki podział ma dobre przełożenie na koszt utrzymania: przy zmianie liczby instancji zmieniasz małą wartość w Terraform, uruchamiasz plan + apply, a resztę pracy powtarzalnie robi Ansible. Nie trzeba pamiętać „którą maszynę już ręcznie podkręcaliśmy”, wszystko jest w kodzie i w pipeline’ach.
Od czego zacząć z IaC, jeśli mam mały zespół i ograniczony czas?
Na początek nie ma sensu budować „enterprise’owego” setupu. Wystarczy prosty zestaw: jedno repozytorium Git (np. infra), najważniejsze moduły Terraform dla chmury, której używasz, oraz kilka podstawowych playbooków Ansible do konfiguracji systemu i deploymentu aplikacji. Backend stanu Terraform może na start leżeć w prostym zdalnym storage (S3, Azure Storage, GCS) bez skomplikowanych blokad.
Dobry pierwszy krok: przenieść jedno istniejące środowisko testowe z „klikanego” na IaC. Na nim przećwiczysz pełny cykl: opis, plan, review, apply oraz odtworzenie od zera. Gdy ten proces zacznie działać w miarę płynnie, dopiero wtedy opłaca się inwestować w bardziej rozbudowane moduły, automatyzację inventory dla Ansible czy rozdzielenie repozytoriów.
Kiedy inwestycja w IaC zaczyna się realnie zwracać?
Największy efekt widać, gdy pojawia się więcej niż jedno środowisko lub gdy konfiguracja często się zmienia. Przykładowo: testy feature branches, osobne środowisko staging, kilka regionów chmurowych albo wymagania dotyczące Disaster Recovery. Każde takie środowisko „z palca” to osobny projekt; w IaC to zwykle wariant istniejącego kodu z kilkoma parametrami.
Krytyczny moment przychodzi przy odtwarzaniu produkcji: awaria, DR, migracja do innego regionu. Bez IaC to bardzo drogi i powolny proces, zależny od ludzi i notatek. Z IaC uruchamiasz znany zestaw definicji, poprawiasz parametry (np. region, rozmiary instancji), a środowisko wstaje przewidywalnie. Ten scenariusz potrafi spłacić początkowy koszt nauki Terraform i Ansible w jedno trudniejsze wdrożenie.
Czy Terraform lub Ansible mogą całkowicie zastąpić ręczne klikanie w konsoli?
W praktyce da się dojść bardzo blisko stanu, w którym konsola chmurowa służy tylko do podglądu, debugowania i wyjątkowych działań awaryjnych. Nowe zasoby, zmiany konfiguracji i skalowanie wykonujesz z kodu za pomocą Terraform i Ansible, przechodząc przez pull requesty i pipeline’y.
Czasem szybciej jest „na szybko” coś kliknąć, szczególnie na początku lub w małych eksperymentach. Klucz w tym, by to, co ma zostać na dłużej (np. nowy serwis, nowa sieć, stałe zmiany w konfiguracji), jak najszybciej przenieść do kodu. W ten sposób unikasz sytuacji, w której produkcja działa inaczej niż to, co masz opisane w repozytorium.
Źródła
- Infrastructure as Code: Managing Servers in the Cloud. O'Reilly Media (2016) – Podstawy IaC, korzyści, wzorce automatyzacji infrastruktury
- Terraform: Up & Running, 3rd Edition. O'Reilly Media (2022) – Praktyczne użycie Terraform, stan, plan/apply, dobre praktyki
- Terraform Documentation. HashiCorp – Oficjalna dokumentacja Terraform, model deklaratywny i zarządzanie stanem
- Ansible Documentation. Red Hat – Oficjalna dokumentacja Ansible, playbooki, role, idempotencja
- The DevOps Handbook: How to Create World-Class Agility, Reliability, and Security. IT Revolution Press (2016) – Praktyki DevOps, automatyzacja, pipeline’y CI/CD i IaC






