Spis treści

Azure IaC zbudowana dla Twoich potrzeb

Wzorce architektoniczne i gotowe narzędzia do dalszej rozbudowy: oparte na OpenTofu i AVM.

Przez lata pracy miałem okazję uczestniczyć w wielu projektach i widzieć różne podejścia do organizacji infrastruktury jako kod przy użyciu Terraforma. Niektóre z nich działały lepiej, inne gorzej. Część świetnie sprawdzała się na początku, ale w miarę rozwoju projektu, przy dodawaniu nowych zasobów pod presją czasu i priorytetów organizacji, kod stawał się trudny w utrzymaniu. Infrastruktura nadal była zapisana w kodzie, ale każda zmiana zajmowała więcej czasu, a refaktoryzacja przestawała być opłacalna.

Dzięki tym doświadczeniom wiem, jak ja sam chciałbym budować infrastrukturę jako kod w nowych projektach i jak podszedłbym do migracji istniejących zasobów chmurowych do repozytoriów.

Pomyślałem też, że seria tych artykułów będzie dobrą okazją do uporządkowania wiedzy o Microsoft Azure: w sposób praktyczny, z realną wartością zarówno dla mnie, jak i dla Ciebie.

Zapraszam Cię zatem do serii “Infrastruktura w skali z Azure i OpenTofu”. Poniżej znajdziesz listę artykułów i to, czego możesz się z nich dowiedzieć.

Opis serii

Ten artykuł jest częścią serii “Infrastruktura w skali z Azure i OpenTofu”. Całość składa się z poniższych wpisów.

  • Azure IaC zbudowana dla Twoich potrzeb (czytasz go właśnie teraz).

    Poznasz podejścia do projektowania infrastruktury jako kod: ich zalety oraz ograniczenia. Zobaczysz szkielet, który możesz wdrożyć u siebie lub w swoim zespole.

  • Zbuduj elastyczne moduły infrastruktury oraz ich rejestr.

    Dowiesz się, jak tworzyć moduły OpenTofu zgodnie z dobrymi praktykami: elastyczne, wersjonowane i gotowe na współdzielenie. Pokażę też, jak uruchomić własny lekki rejestr.

  • Zaprojektuj CI/CD dla kodu Twojej infrastruktury jako kod.

    Zobaczysz, jak może wyglądać skuteczne CI/CD dla Twojej infrastruktury. Omówimy narzędzia, schematy integracji i automatyzacje ułatwiające rozwój, wdrożenia i utrzymanie.


Założenia

Zanim zaczniemy, ustalmy kilka podstawowych założeń. W tej serii korzystam z:

  • Microsoft Azure - bo chcę przy okazji uporządkować swoją wiedzę z tej chmury, tworząc coś realnie przydatnego.
  • OpenTofu - bo to w pełni open source’owa alternatywa dla Terraforma.
  • GitHub - bo to najczęściej wybierane narzędzie do hostowania kodu i automatyzacji CI/CD.

Choć seria koncentruje się na Microsoft Azure, OpenTofu i GitHubie, większość omawianych wzorców możesz zaadaptować do innych chmur, używać z Terraformem, GitLabem czy dowolnym innym narzędziem CI/CD.


Jak można zaprojektować IaC w Azure

Zacznijmy od przeglądu podejść do organizowania infrastruktury jako kod w Microsoft Azure, z użyciem OpenTofu. Pokażę Ci kilka modeli, z którymi spotkałem się w praktyce: wraz z ich zaletami i ograniczeniami.

Po tej sekcji przedstawię podejście, które sam wybrałem do tworzenia infrastruktury jako kod w moich projektach.

Uwaga
W przykładach celowo nie używam for_each ani count. Chcę, aby skupiały się one na strukturze repozytoriów, wersjonowaniu modułów oraz granicach stanu, a nie na detalach implementacyjnych. Te mechanizmy pojawią się w kolejnych wpisach z serii.

Monorepo

W tym podejściu cała infrastruktura jako kod znajduje się w jednym repozytorium. Najczęściej jest ona podzielona na środowiska (na przykład dev (1️⃣) i prod (2️⃣)), a w nich na katalogi, takie jak databases (3️⃣) czy network (4️⃣), w których definiuje się zasoby (resource) i/lub moduły (module). Pliki stanu zawierają wiele elementów, co z czasem utrudnia ich utrzymanie.

Przykładowa struktura repozytorium w takim podejściu może wyglądać tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
tofu-infrastructure
├── dev 1️⃣
│   ├── databases 3️⃣
│   │   ├── main.tf
│   │   └── providers.tf
│   └── network 4️⃣
│       ├── main.tf
│       └── providers.tf
└── prod 2️⃣
    ├── databases 3️⃣
    │   ├── main.tf
    │   └── providers.tf
    └── network 4️⃣
        ├── main.tf
        └── providers.tf

A tak typowe pliki środowiskowe.

  • prod/databases/main.tf
1
2
3
4
5
6
7
resource "azurerm_postgresql_flexible_server" "checkout_db" {
  /* Pozostały kod pominięty dla przejrzystości */
}

resource "azurerm_postgresql_flexible_server" "shipping_db" {
  /* Pozostały kod pominięty dla przejrzystości */
}
  • dev/databases/main.tf
1
2
3
4
5
6
7
resource "azurerm_postgresql_flexible_server" "checkout_db" {
  /* Pozostały kod pominięty dla przejrzystości */
}

resource "azurerm_postgresql_flexible_server" "shipping_db" {
  /* Pozostały kod pominięty dla przejrzystości */
}

Dodanie nowego zasobu wymaga zadeklarowania go we wszystkich odpowiednich miejscach.

Wdrożenie nowego środowiska polega na utworzeniu dodatkowego folderu w monorepo i zdefiniowaniu w nim wymaganych zasobów.

Dodawanie nowych funkcjonalności lub refaktoryzacja często wiążą się ze zmianami w wielu miejscach – zarówno w ramach jednego środowiska, jak i pomiędzy nimi. Ten rozproszony zakres zmian zwiększa nakład pracy, przez co takie inicjatywy często nie są realizowane.

Podbijanie wersji providerów lub modułów wymaga modyfikacji w wielu folderach jednocześnie, co zniechęca do bieżącej aktualizacji i zwiększa ryzyko zaległości technicznych.

Zalety i ograniczenia

Szybki start. Idealne na proof of concept lub krótkie eksperymenty.

Chaos przy większej skali. Wzrost liczby zasobów utrudnia utrzymanie i aktualizacje.

Duży stan. Duże pliki stanu spowalniają pracę i utrudniają wdrażanie zmian.

Kiedy wybrać?
Moim zdaniem: tylko dla bardzo małych projektów lub własnych eksperymentów. W przypadku produkcyjnych rozwiązań zdecydowanie je odradzam.

Monorepo + lokalne moduły

W tym podejściu zasoby definiowane są jako lokalne moduły. Repozytorium zawiera katalog modules (1️⃣), w którym znajdują się wersjonowane moduły infrastrukturalne. Każde środowisko (np. dev (2️⃣), prod (3️⃣)) posiada osobne katalogi z plikami stanu, które wykorzystują te moduły.

Pliki stanu nadal obejmują wiele zasobów, co z czasem utrudnia pracę i ogranicza niezależność zmian.

W praktyce wygląda to tak, jak poniżej:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
tofu-infrastructure
├── environments
│   ├── dev 2️⃣
│   │   ├── databases
│   │   │   ├── main.tf
│   │   │   └── providers.tf
│   │   └── network
│   │       ├── main.tf
│   │       └── providers.tf
│   └── prod 3️⃣
│       ├── databases
│       │   ├── main.tf
│       │   └── providers.tf
│       └── network
│           ├── main.tf
│           └── providers.tf
└── modules 1️⃣
    ├── database-1.0
    ├── database-1.1
    ├── database-1.2
    └── vnet-1.0

A tak typowe wywołanie modułów.

  • environments/prod/databases/main.tf:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module "checkout_db" {
  source = "../../../modules/database-1.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

module "shipping_db" {
  source = "../../../modules/database-1.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

/* Pozostały kod pominięty dla przejrzystości */
  • environments/dev/databases/main.tf:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module "checkout_db" {
  source = "../../../modules/database-1.1"

  /* Pozostały kod pominięty dla przejrzystości */
}

module "shipping_db" {
  source = "../../../modules/database-1.2"

  /* Pozostały kod pominięty dla przejrzystości */
}

/* Pozostały kod pominięty dla przejrzystości */

Dodanie kolejnych zasobów polega zazwyczaj na ponownym wywołaniu istniejącego modułu. Dzięki temu kod jest mniej zduplikowany – powtarzają się jedynie deklaracje modułów.

Wdrożenie nowego środowiska wymaga utworzenia katalogu i dodania w nim wywołań lokalnych modułów.

Gdy potrzebna jest zmiana w module albo jego refaktoryzacja, tworzy się nowy katalog z nową wersją (np. database-1.1) i tam wprowadza modyfikacje. Jednak przy rosnącej liczbie wersji modułów i aplikacji, z czasem narasta dług techniczny związany z testowaniem oraz utrzymywaniem zgodności między wersjami.

Aktualizacja wersji providerów lub modułów nadal wymaga zmian w wielu miejscach jednocześnie, ale dzięki modularności jest to bardziej kontrolowalne niż wcześniej. Jeżeli chcemy zachować jednolitą wersję modułu we wszystkich środowiskach, trzeba zaktualizować każdą referencję.

Zalety i ograniczenia

Lepsze współdzielenie logiki. Lokalne moduły można ponownie używać, co sprzyja standaryzacji.

Większa czytelność niż w czystym monorepo. Katalog modules tworzy pewną strukturę.

Fałszywe poczucie porządku. Użycie modułów sprawia wrażenie dobrej architektury, ale bez jasnych konwencji projekt może się szybko skomplikować.

Koszt aktualizacji i testowania. Aktualizacje wersji to wielokrotne zmiany i konieczność utrzymania wielu wersji lokalnego modułu zanim nowa wersja zostanie dodana we wszystkich miejscach.

Wciąż zbyt wiele zasobów w jednym stanie. Pliki stanu są nadal zbyt obszerne, co utrudnia równoległy rozwój.

Kiedy wybrać?
Moim zdaniem: dla bardzo małych zespołów i niewielkiej infrastruktury to rozwiązanie może sprawdzać się nawet w środowisku produkcyjnym. Przy większej skali utrzymywanie wielu wersji lokalnych modułów szybko staje się uciążliwe.

Repo per usługa + repo per module

To podejście jest przeciwieństwem monorepo. Przypomina to architekturę mikroserwisów: każda część jest izolowana i zarządzana osobno. Każde repozytorium odpowiada za konkretną usługę lub obszar infrastruktury (np. tofu-networking dla sieci, tofu-databases dla warstwy danych (1️⃣)). Wewnątrz każdego z nich znajdują się katalogi środowiskowe (np. dev (2️⃣) i prod (3️⃣)).

Repozytoria zawierają wywołania modułów, natomiast sama logika tworzenia zasobów znajduje się w osobnych repozytoriach z modułami (np. terraform-azurerm-avm-res-resources-resourcegroup z Azure Verified Modules lub w prywatnym rejestrze w Twojej organizacji). Moduły mogą być wywoływane z rejestru (po wersji) lub bezpośrednio z repozytorium (po commicie lub tagu), zgodnie z dokumentacją OpenTofu.

Można też trzymać moduły lokalnie w katalogu modules wewnątrz repozytorium usługi. Warto jednak pamiętać o ich ograniczeniach opisanych wcześniej.

Chociaż same repozytoria są mniejsze, pliki stanu często obejmują wiele zasobów, co z czasem może utrudniać utrzymanie.

Przykładowa struktura repozytorium dla warstwy baz danych:

1
2
3
4
5
6
7
tofu-databases 1️⃣
├── dev 2️⃣
│   ├── main.tf
│   └── providers.tf
└── prod 3️⃣
    ├── main.tf
    └── providers.tf

Typowe pliki środowiskowe:

  • prod/main.tf:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module "checkout_db" {
  source  = "modules.example.net/database/azurerm"
  version = "1.0.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

module "shipping_db" {
  source  = "modules.example.net/database/azurerm"
  version = "1.0.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

/* Pozostały kod pominięty dla przejrzystości */
  • dev/main.tf:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module "checkout_db" {
  source  = "modules.example.net/database/azurerm"
  version = "1.1.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

module "shipping_db" {
  source  = "modules.example.net/database/azurerm"
  version = "1.2.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

/* Pozostały kod pominięty dla przejrzystości */

Dodawanie nowych zasobów przez moduły jest szybkie. Mniejsze repozytoria ograniczają konflikty i blokowanie pracy: typowe problemy dużego monorepo.

Trzeba jednak pamiętać, że duża liczba repozytoriów dedykowanych konkretnym usługom może być uciążliwa przy tworzeniu nowych środowisk. W takim przypadku w każdym z tych repozytoriów trzeba utworzyć pull request z dodaniem nowego folderu środowiska.

Zmiany w module nie wpływają bezpośrednio na kod repozytorium usługi, co upraszcza rozwój i testowanie. Trzeba jednak uważać na zależności: jeśli moduł A udostępnia outputy wykorzystywane przez moduł B, zmiana outputów wymaga odświeżenia stanu modułu zależnego.

Warto zadbać o CI/CD dla modułów, co przyspiesza ich rozwój i poprawia jakość.

Utrzymanie wymaga dyscypliny. Wersje modułów i providerów trzeba aktualizować w wielu repozytoriach. Automatyzacja (np. Renovate) i jasne praktyki zespołowe znacząco to ułatwiają.

Zalety i ograniczenia

Niski próg wejścia. Dzięki niewielkim rozmiarom repozytoriów, nowy inżynier szybko zrozumie, za co odpowiada konkretne repo.

Bezpieczna refaktoryzacja. Zmiany logiczne nie wpływają bezpośrednio na stan środowisk. Można testować osobno.

Złożony onboarding środowiska. Dodanie nowego środowiska wymaga zmian w wielu repozytoriach (i wielu PR-ach).

Rosnący stan. Duże pliki stanu nadal ograniczają pracę równoległą i spowalniają plan / apply.

Kiedy wybrać?
Moim zdaniem: dla większych zespołów i bardziej rozbudowanej infrastruktury, szczególnie gdy nie zachodzi potrzeba częstego dodawania i usuwania środowisk. Warto zadbać o CI/CD dla modułów oraz automatyzację aktualizacji wersji.

Monorepo + repo per module

Podejście łączące centralizację root modułów z wersjonowaniem logiki infrastruktury w osobnych repozytoriach modułów. Root moduły (zawierające pliki stanu) znajdują się w jednym repozytorium. Typowo struktura jest podzielona według środowisk (dev (1️⃣), prod (2️⃣), itd.). Wewnątrz każdego z nich znajdują się katalogi definiujące obszary infrastruktury (np. databases (3️⃣) i network (4️⃣)).

Moduły infrastrukturalne umieszczone są w osobnych repozytoriach (jak w poprzednim podejściu) i mogą być wywoływane ze zdalnego rejestru (z określoną wersją) lub bezpośrednio z repozytorium (poprzez commit lub tag).

Dzięki centralizacji plików stanu, koordynacja wdrożeń między zespołami jest łatwiejsza. Jednak pliki stanu nadal często obejmują wiele zasobów, co może utrudniać niezależny rozwój i testowanie.

Przykładowa struktura jest podobna do już wcześniej przedstawionych. Może wyglądać następująco:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
tofu-infrastructure
├── dev 1️⃣
│   ├── databases 3️⃣
│   │   ├── main.tf
│   │   └── providers.tf
│   └── network 4️⃣
│       ├── main.tf
│       └── providers.tf
└── prod 2️⃣
    ├── databases 3️⃣
    │   ├── main.tf
    │   └── providers.tf
    └── network 4️⃣
        ├── main.tf
        └── providers.tf

Typowe wywołania modułów w środowiskach wyglądają następująco:

  • prod/databases/main.tf:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module "checkout_db" {
  source  = "modules.example.net/database/azurerm"
  version = "1.0.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

module "shipping_db" {
  source  = "modules.example.net/database/azurerm"
  version = "1.0.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

/* Pozostały kod pominięty dla przejrzystości */
  • dev/databases/main.tf:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
module "checkout_db" {
  source  = "modules.example.net/database/azurerm"
  version = "1.1.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

module "shipping_db" {
  source  = "modules.example.net/database/azurerm"
  version = "1.2.0"

  /* Pozostały kod pominięty dla przejrzystości */
}

/* Pozostały kod pominięty dla przejrzystości */

Dodawanie kolejnych zasobów, podobnie jak w monorepo z lokalnymi modułami, polega zazwyczaj na ponownym wywołaniu istniejącego modułu. Dzięki modularności unika się powielania logiki.

Dodanie nowego środowiska polega na utworzeniu folderu w strukturze katalogów oraz inicjalizacji backendu stanu. Dzięki centralizacji wszystko znajduje się w jednym repozytorium, co przyspiesza onboarding (o ile struktura katalogów jest dobrze zorganizowana).

Zmiany w module wymagają utworzenia nowej wersji (np. 1.1.0) i jej wdrożenia w konkretnym katalogu środowiskowym. Dzięki temu dodawanie nowych funkcjonalności lub przeprowadzanie refaktoryzacji jest proste do zrobienia. Należy jednak pamiętać o CI/CD dla modułów, które ułatwią dostarczanie.

Utrzymanie jest umiarkowanie złożone. Wersjonowanie modułów zapewnia przewidywalność zmian. Jednocześnie centralizacja stanu może sprawić, że zmiany w komponentach wspólnych (np. aktualizacja providera) będą miały szerszy zasięg i wymagają większej uwagi przy testowaniu.

Zalety i ograniczenia

Jedno źródło prawdy. Wszyscy widzą całość infrastruktury w jednym miejscu.

Prostszy onboarding środowiska. Dodanie nowego środowiska (np. trial lub test) wymaga jedynie stworzenia nowego folderu w monorepo i dodaniu wywołań potrzebnych modułów.

Prostszy rozwój i utrzymanie. Dodawanie nowej logiki lub refaktoryzacja istniejącego kodu są bezpieczniejsze i bardziej komfortowe.

Zbyt duże pliki stanu. Centralizacja stanu może skutkować konfliktem przy zmianach, wolniejszym plan i problemami z równoległym developmentem.

Kiedy wybrać?
Moim zdaniem: To podejście może sprawdzić się dobrze w średnich oraz dużych zespołach, które chcą zachować centralne repozytorium, ale korzystać z elastyczności i wersjonowania modułów. Wymaga dobrej dyscypliny w strukturze folderów, CI/CD dla modułów oraz automatyzacji aktualizacji wersji.

Porównanie

Podsumowując tą sekcję, zestawiłem wszystkie podejścia w jednej tabeli, aby ułatwić dobór strategii dopasowanej do specyfiki zespołu, skali środowiska oraz sposobu zarządzania infrastrukturą.

PodejścieDodawanie zasobówOnboarding środowiskaRefactoringUtrzymanieWielkość stanu
Monoreposzybkieszybkitrudnytrudneduża
Monorepo + lokalne modułyszybkieszybkiśredniśrednieduża
Repo per usługa + repo per moduleszybkiewolnyłatwyśrednieśrednia
Monorepo + repo per moduleszybkieszybkiłatwyśrednieduża

Poniższy diagram radarowy wizualizuje kluczowe cechy każdego podejścia, ułatwiając wybór najlepiej dopasowanej strategii.


Jak ja projektuję IaC w Azure

Po omówieniu różnych strategii organizacji infrastruktury jako kodu, czas pokazać strukturę wykorzystywaną przeze mnie: skalowalną, modularną i zgodną z Azure Verified Modules. Bazuje ona na podejściu Monorepo + repo per module, ale została wzbogacona o kilka istotnych niuansów.

Całość dzieli się na dwie części:

  • Core
  • Infrastruktura aplikacji

W kolejnych sekcjach opisuję szczegółowo każdą z nich.

Core: organization-template

Część monorepo nazywam organization-template i traktuję jako solidny punkt wyjścia dla każdej organizacji, niezależnie od skali czy złożoności. Znajdziesz tam gotowy do użycia kod OpenTofu oparty o Azure Verified Modules, który pomoże Ci stworzyć fundamenty dla Twojej organizacji w Microsoft Azure.

Zawartość

W skład organization-template wchodzą między innymi:

Struktura jest prosta: czytelna, łatwa w utrzymaniu i gotowa do rozbudowy.

Konwencja nazewnicza

W głównym katalogu znajdują się katalogi odpowiedzialne za poszczególne obszary infrastruktury. Nazwane są zgodnie z poniższą konwencją:

Na przykład:

Jeżeli dwa lub więcej katalogów mają ten sam numer, na przykład 04-backupvaults i 04-networking-nsgs mogą być wykonywane równolegle, ponieważ nie mają między sobą zależności.

Dzięki tej konwencji nazewniczej w głównym katalogu od razu wiadomo, jaka jest kolejność wykonywania. Taka numeracja i struktura katalogów pozwala z jednej strony jasno odczytać kolejność, a z drugiej umożliwia równoległe wykonywanie części modułów, co skraca czas wdrożeń.

W przypadku rozbudowy o kolejne obszary dodaje się nowy katalog z odpowiednią numeracją, w razie potrzeby zmieniając istniejącą numerację. Jeżeli poszczególne obszary nie są od siebie zależne mogą być umieszczane na tym samym poziomie poprzez dodanie tego samego numeru.

Hierarchia

Każdy obszar infrastruktury ma odpowiednią hierarchię:

Na przykład:

Hierarchia ta nawiązuje do konwencji nazewniczej zasobów w Microsoft Azure. Na przykład:

1
/subscriptions/${subscription-id}/resourceGroups/rg-default-eastus/providers/Microsoft.Network/virtualNetworks/vnet-default-eastus

Ułatwia to nawigację po strukturze katalogów.

W podejściu, które preferuję stany są mniejsze. Na przykład każdy VNet ma swój osobny stan.

Dzięki temu zmiany są szybsze do wprowadzenia, a przy okazji kod jest łatwiejszy do zrozumienia. Mniejsze stany ułatwiają też równoległe wdrażanie i uniknięcie konfliktów między zespołami.

Ponadto, ścieżki do stanów w Storage Account odpowiadają ścieżką w projekcie. Na przykład dla 03-resourcegroups/your-subscription/rg-default-eastus wygląda to następująco:

1
2
3
4
5
6
terraform {
  backend "azurerm" {
    key = "03-resourcegroups/your-subscription/rg-default-eastus/terraform.tfstate"
    /* Pozostały kod pominięty dla przejrzystości */
  }
}

Zawartość root modułu

Root moduł to katalog, który inicjuje backend stanu (blok terraform) i zarządza konkretnym zestawem zasobów. Typowo zawiera on pliki takie jak:

  • data.tf - data lookups oraz wykorzystanie remote states z innych root modułów.
  • locals.tf - powtarzalne wartości są definiowane tutaj.
  • main.tf - wywołanie modułów i/lub tworzenie zasobów.
  • outputs.tf - zwracanie wartości potrzebne w innych root modułach.
  • providers.tf - deklaracja providerów oraz remote state.

Root moduły do nadawania nazw zasobą wykorzystują moduł naming, który generuje zgodne z konwencją nazwy zasobów w Microsoft Azure. Dla zasobów, które nie są aktualnie wspierane w tym module nazwy są tworzone zgodnie z zaleceniami dotyczącymi skrótów dla zasobów platformy Azure.

Do tego w root modułach wykorzystane są Azure Verified Modules. Na przykład 05-networking-vnets/your-subscription/rg-default-eastus/vnet-default-eastus korzysta z avm-res-network-virtualnetwork, a jego kod wygląda tak, jak poniżej:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
module "naming" {
  source  = "Azure/naming/azurerm"
  version = "0.4.2"

  suffix = ["apps"]
}

module "vnet" {
  source  = "Azure/avm-res-network-virtualnetwork/azurerm"
  version = "0.10.0"

  address_space = ["10.0.0.0/16"]
  # Resource group location can be used from other module via remote state:
  location = data.terraform_remote_state.rg_default_eastus.outputs.resource.location
  # VNet name set with the help of the naming module:
  name = module.naming.virtual_network.name
  # Resource group name can be used from other module via remote state:
  resource_group_name = data.terraform_remote_state.rg_default_eastus.outputs.resource.name
  subnets = {
    "${module.naming.subnet.name}-1" = {
      name             = "${module.naming.subnet.name}-1"
      address_prefixes = ["10.0.0.0/24"]

      # NSG can be used from other module via remote state:
      network_security_group = data.terraform_remote_state.nsg_default_apps.outputs.resource
    }
    "${module.naming.subnet.name}-2" = {
      name             = "${module.naming.subnet.name}-2"
      address_prefixes = ["10.0.1.0/24"]

      # NSG can be used from other module via remote state:
      network_security_group = data.terraform_remote_state.nsg_default_apps.outputs.resource
    }
  }
  /* Pozostały kod pominięty dla przejrzystości */
}

Świadomie nie tworzę własnych wrapperów modułowych w miejscach, gdzie istniejące zasoby są proste lub istnieje perspektywa pojawienia się oficjalnego modułu AVM.

Moim celem jest dostarczenie użytkownikowi jak najbardziej natywnego i idiomatycznego doświadczenia pracy z Microsoft Azure, zgodnego z AVM i gotowego do migracji na oficjalne moduły, gdy tylko się pojawią.

Zależności między root modułami

Jak już pewnie zdążyłeś zauważyć, root moduły zamiast hardkodować wartości argumentów używają wartości zwróconych z innych root modułów. Na przykład:

1
2
# Resource group name can be used from other module via remote state:
resource_group_name = data.terraform_remote_state.rg_default_eastus.outputs.resource.name

Dzieje się to użyciu remote state file, jak poniżej:

1
2
3
4
5
6
7
data "terraform_remote_state" "rg_default_eastus" {
  backend = "azurerm"
  config = {
    key = "03-resourcegroups/your-subscription/rg-default-eastus/terraform.tfstate"
    /* Pozostały kod pominięty dla przejrzystości */
  }
}

Oraz dzięki zadeklarowaniu outputów w innym root module. Na przykład w root module 03-resourcegroups/your-subscription/rg-default-eastus:

1
2
3
output "resource" {
  value = module.rg.resource
}

Kolejność wykonania

Skoro już poznałeś zawartość organization-template, konwencję nazewniczą, hierarchię, zawartość root modułów i zależności między nimi, pora przejść do tworzenia faktycznych zasobow. Root moduły powinny być uruchamiane zgodnie z ich numeracją w katalogu głównym projektu. Numeracja ta odzwierciedla zależności między modułami i pozwala na równoległe wykonanie tych, które ich nie mają.

Poniższy diagram stanów pokazuje zależności i kolejności wykonania root modułów.

Przypomnienie
Dla przypomnienia, katalogi o tym samym numerze (np. 04-backupvaults i 04-networking-nsgs) mogą być wykonywane równolegle, ponieważ nie mają między sobą zależności.

Rozbudowa infrastruktury

Projekt organization-template został celowo zaprojektowany jako niewielki i zwięzły: stanowi fundament, który można bezpiecznie rozbudowywać. W przypadku chęci dodawania kolejnych obszarów infrastruktury zachęcam Cię do podążania za konwencją nazewniczą, tworzenia niedużych stanów, używania Azure Verified Modules tam, gdzie to możliwe oraz przekazywania wartości z jednego modułu do drugiego przy pomocy outputów i remote state.

Infrastruktura aplikacji

Przeglądając organization-template może pojawić się pytanie: gdzie umieścić infrastrukturę aplikacji? Na przykład maszynę wirtualną, Storage Account, bazę danych, Azure Function. Te wszystkie komponenty w podejściu, które preferuję są utrzymywane razem z kodem aplikacji.

Dzięki temu zespół aplikacyjny samodzielnie zarządza swoją infrastrukturą we wspólnym repozytorium i cyklu życia, obok kodu aplikacji. To skraca czas dostarczania i minimalizuje zależności od zespołu platformowego.

W tym podejściu w głównym folderze repozytorium znajduje się katalog infra (1️⃣), który zawiera podkatalogi ze środowiskami, gdzie aplikacja jest (lub może być zdeployowana), na przykład dev (2️⃣) i prod (3️⃣). Kod aplikacji znajduje się natomiast w katalogu src lub innym zgodnym z Twoją konwencją lub konwencją danego języka programowania.

Przykładowa struktura w takim przypadku wygląda, jak poniżej:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
app-repo
├── infra 1️⃣
│   ├── dev 2️⃣
│   │   ├── data.tf
│   │   ├── main.tf
│   │   ├── outputs.tf
│   │   └── providers.tf
│   └── prod 3️⃣
│       ├── data.tf
│       ├── main.tf
│       ├── outputs.tf
│       └── providers.tf
├── README.md
└── src

Dodawanie nowego zasobu do aplikacji wiąże się z dodaniem go w poszczególnych katalogach środowisk. Aby uniknąć zjawiska “configuration drift”, warto opakować zasoby w moduły i umieścić je w prywatnym rejestrze. Jeśli kilka aplikacji potrzebuje tych samych zasobów np. bazy danych oraz storage account, warto jest stworzyć dla nich wspólny moduł.

Dodawanie nowego środowiska wymaga stworzenia nowego folderu w katalogu infra, a następnie wywołania odpowiednich modułów.

Jeżeli jest potrzeba zmian w logice, tworzy się nową wersję modułu, a następnie wdraża się ją stopniowo do wszystkich środowisk. Tak samo, jak w podejściach repo per usługa + repo per module albo monorepo + repo per module.

Utrzymanie również przypomina poprzednie podejścia: aktualizacje wersji modułów lub providerów są wykonywane bezpośrednio w katalogach środowisk.

Chcesz więcej?
Żeby dowiedzieć się więcej o tych modułach oraz o rejestrze zapraszam Cię do kolejnego artykułu z tej serii: Zbuduj elastyczne moduły infrastruktury oraz ich rejestr.

Gdy organization-template wystarczy

W niektórych przypadkach oddzielenie głównej infrastruktury od infrastruktury aplikacyjnej nie jest konieczne ani optymalne. Dotyczy to zwłaszcza organizacji, które rozwijają jedną, monolityczną aplikację wdrażaną na kilku środowiskach (np. dev, test, prod) lub takich, gdzie zespoły aplikacyjne nie czują się swobodnie w pracy z kodem infrastruktury.

W takich sytuacjach bardziej praktycznym i skalowalnym podejściem może być trzymanie całej infrastruktury (zarówno platformowej, jak i aplikacyjnej) w jednym repozytorium. Upraszcza to zarządzanie, przyspiesza wdrożenia i zmniejsza próg wejścia dla zespołu.

Porównanie

Wracając do wcześniejszych podejść, poniżej przedstawiam tabelę porównującą moje podejście z poprzednimi.

PodejścieDodawanie zasobówOnboarding środowiskaRefactoringUtrzymanieWielkość stanu
Monoreposzybkieszybkitrudnytrudneduża
Monorepo + lokalne modułyszybkieszybkiśredniśrednieduża
Repo per usługa + repo per moduleszybkiewolnyłatwyśrednieśrednia
Monorepo + repo per moduleszybkieszybkiłatwyśrednieduża
👉 Moje podejścieszybkieszybkiłatwyśredniemały

Poniższy diagram radarowy ilustruje te różnice w sposób wizualny.


Podsumowanie

W tym wpisie poznałeś różne podejścia do organizowania infrastruktury jako kod na platformie Microsoft Azure przy użyciu OpenTofu. Od prostego monorepo, przez lokalne moduły, aż po wersjonowane moduły i repozytoria per usługa: każde z nich ma swoje zalety i ograniczenia. Nie istnieje rozwiązanie uniwersalne. Wybór zależy od potrzeb zespołu, skali organizacji i sposobu pracy.

Pokazałem Ci też, jak sam podchodzę do tego tematu: używam organization-template jako fundamentu, a infrastrukturę aplikacyjną trzymam bezpośrednio w repozytorium aplikacji. To połączenie daje skalowalność, przejrzystość i prostsze utrzymanie bez odbierania zespołom aplikacyjnym autonomii.

To jednak dopiero początek. W kolejnej części tej serii pokażę, jak tworzę wersjonowane moduły zgodne z AVM oraz jak buduję lekki rejestr, który upraszcza współdzielenie i rozwój infrastruktury między zespołami.

Co dalej?

Spodobał Ci się koncept organization-template?

👉 Skorzystaj z przycisku Use this template albo zrób forka i sprawdź, jak ten szkielet zadziała w Twojej organizacji.

Masz pomysł na rozwój?

👉 Przejrzyj CONTRIBUTING.md i zobacz, jak możesz się włączyć.

Znalazłeś błąd?

👉 Zgłoś issue tutaj.

Do you like this blog and my work? Buy Me A Coffee