Refaktoryzacja HCL w całej organizacji z użyciem OpenRewrite
Bezpieczne migracje modułów AVM i OpenTofu w praktyce.

W pewnym momencie każdy większy projekt infrastruktury jako kod dochodzi do punktu, w którym zmiany przestają być lokalne. Aktualizacja wersji modułu, zmiana kontraktu wejściowego albo migracja pomiędzy podejściami architektonicznymi wymaga wtedy modyfikacji w wielu miejscach jednocześnie oraz często w kilku repozytoriach.
W teorii są to “proste” zmiany: zmiana wersji, usunięcie parametru, dodanie nowego bloku konfiguracyjnego. W praktyce jednak ręczne refaktoryzowanie takich zmian jest czasochłonne, podatne na błędy i trudne do bezpiecznego review. Skrypty oparte o grep, sed czy inne operacje tekstowe pomagają tylko do pewnego momentu. Nie rozumieją one struktury konfiguracji, są wrażliwe na formatowanie i, co najważniejsze, nie są deterministyczne. Każde kolejne uruchomienie takiego skryptu wymaga ponownej weryfikacji efektu końcowego.
W tym artykule pokazuję, jak podejść do migracji infrastruktury jako kod w sposób deterministyczny, powtarzalny i bezpieczny, wykorzystując refaktoryzację opartą o strukturalną analizę konfiguracji, zachowującą jej semantykę i formatowanie. Na przykładzie rzeczywistych migracji modułów Azure Verified Modules (AVM) pokazuję, jak przygotować zmiany, które trafiają do pull requesta jako czytelny diff zamiast ręcznej pracy rozproszonej po repozytorium.
Dlaczego OpenRewrite
Kluczową różnicą pomiędzy podejściem opartym o refaktoryzację strukturalną a klasycznymi narzędziami tekstowymi jest to, że operujemy na semantycznej reprezentacji konfiguracji, a nie na jej surowej postaci tekstowej. OpenRewrite wykorzystuje tzw. lossless semantic trees (LST), czyli strukturę, która zachowuje pełną informację o oryginalnym pliku, w tym formatowanie, komentarze i kolejność elementów, jednocześnie umożliwiając bezpieczne modyfikacje semantyczne.
W praktyce oznacza to, że zmiany są aplikowane na rzeczywistych elementach konfiguracji. Na przykład modułach, parametrach czy blokach, a nie na fragmentach tekstu. Dzięki temu refaktoryzacje są deterministyczne. To samo wejście zawsze prowadzi do tego samego wyniku, niezależnie od stylu formatowania. Są również idempotentne. Wielokrotne uruchomienie tej samej migracji nie powoduje duplikacji ani narastania zmian.
Istotnym efektem tego podejścia jest jakość wyniku. OpenRewrite zachowuje oryginalną strukturę plików, dzięki czemu wynik migracji trafia do repozytorium jako czytelny, przewidywalny diff, gotowy do standardowego code review. Migracja staje się w ten sposób normalną operacją inżynierską, zakończoną pull requestem, a nie jednorazowym skryptem wymagającym ręcznego sprawdzania.
Warto też zauważyć, że to podejście nie jest ograniczone do HCL ani OpenTofu / Terraform. OpenRewrite oferuje analogiczne, semantycznie świadome refaktoryzacje dla innych formatów konfiguracyjnych, w tym YAML-i wykorzystywanych w ekosystemie Kubernetes (Helm, Flux, Argo CD).
Oznacza to, że ten sam model pracy, czyli jawne recipes, deterministyczne wykonanie i pull request jako wynik, może być zastosowany również do migracji manifestów Kubernetesowych, zmian API czy refaktoryzacji konfiguracji GitOps. To temat na osobne case study.
Case study
Prosty przykład
Zanim przejdziesz do bardziej złożonego scenariusza, przyjrzyj się prostemu przykładowi. Moduł avm-res-network-virtualnetwork przeszedł w wersji 0.11.0 zmianę kontraktu: usunięcie resource_group_name i subscription_id, dodanie wymaganego parent_id. To typowe zmiany inżynierskie: proste koncepcyjnie, ale ryzykowne do zastosowania ręcznie w wielu miejscach.
Migracja jest opisana jako OpenRewrite recipe:
| |
To wystarczy. Migracja jest niezmienna, powtarzalna i gotowa do zastosowania w całej organizacji.
Złożony scenariusz
Teraz przejdźmy do rzeczywistości. Moduł avm-res-network-privatednszone w wersji 0.4.0 przeszedł gruntowną refaktoryzację. Zmienił się nie tylko kontrakt, ale architektura samego modułu: przejście z zasobów Azurem na azapi, używanie bloków removed i import do migracji stanu. To już nie jest prosta zmiana, to jest złożona, wieloetapowa refaktoryzacja.
Co się musiało zmienić
W starej wersji (0.3.x) moduł zarządzał zasobami bezpośrednio przez zasoby Azurerm: azurerm_private_dns_zone, azurerm_private_dns_zone_virtual_network_link, rekordy DNS (azurerm_private_dns_a_record, itp.) oraz przypisania ról.
W nowej wersji (0.4.0) wszystko przesunęło się na azapi_resource. Oznacza to, że zasoby, które istnieją w stanie OpenTofu / Terraform z wersji 0.3.x, muszą zostać przeniesione do nowej reprezentacji: nie usunięte i ponownie utworzone. Dla infrastruktury produkcyjnej to jest krytyczne: zamiast destrukcji i rekonstrukcji chcemy dać znać OpenTofu / Terraform: “ten zasób istnieje tam gdzie jest, ale teraz będziemy nim zarządzać inaczej”.
Struktura migracji
Recipe YAML dla tej migracji robi pięć rzeczy:
1. Przygotowanie środowiska: dodanie wymaganych providerów (azapi, modtm).
| |
2. Podbicie wersji: z ~> 0.3.5 do ~> 0.4.0.
| |
3. Konwersja locals: z typu string na list.
| |
4. Modyfikacja parametrów wejściowych: usunięcie resource_group_name i dodanie parent_id.
| |
5. Migracja zasobów: trzy warianty dla każdego zasobu.
| |
Proces jest powtórzony dla każdego zasobu: stref DNS, linków sieciowych, przypisań ról, rekordów DNS (A, AAAA, CNAME, MX, PTR, SRV, TXT).
Dlaczego to wiele linii?
Recipe dla privatednszone zawiera ~180 linii, głównie powtórzenia tego samego wzorca aplikowanego do różnych zasobów. Czy to słaba konstrukcja? Nie. To jest jawna, deklaratywna lista zmian. Każda linia odpowiada jednej operacji, którą chcemy przeprowadzić. Na etapie code review dokładnie widać: “Ah, migrujemy osiem typów rekordów DNS, każdy zostanie usunięty ze stanu, a następnie importowany”.
W praktyce, gdy uruchamiasz rewriteDryRun:
| |
Widzisz efekt: nie sześć kopii tego samego bloku repetycyjnie, lecz konkretne wpisy dla każdego zasobu, które znajdują się w Twoim kodzie.
Wynik migracji
Po zastosowaniu recipe YAML wynikowy diff pokazuje:
| |
Jak widać powyżej, zmiany dotyczą trzech plików:
main.tf: wersja modułu zmienia się z
~> 0.3.5na~> 0.4.0, parametrresource_group_namezostaje usunięty, pojawia sięparent_id, oraz blokiremovediimportdla każdego zasobu. Z przyczyn przejrzystości pokazano tylko przykłady dla stref DNS. Pattern ten powtarza się identycznie dlaaaaa_record,cname_record,mx_record,ptr_record,srv_recorditxt_record.locals.tf: wartości tekstowe dla rekordów DNS (np.
txt_records) zostają automatycznie przekonwertowane na listy stringu:"banana"zmienia się na["banana"]. To jest niezbędne, ponieważ nowa wersja modułu oczekuje tego formatu.terraform.tf: dodaje się provider
azapiw wymaganej konfiguracji. To jest kluczowe, ponieważ nowa wersja modułu używa zasobówazapi_resourcezamiast zasobów Azurerm.
OpenTofu / Terraform w plan pokaże: “Resource will be imported”. Po apply zasób zostanie ponownie zarejestrowany bez zmiany rzeczywistego stanu na Azure.
Powtarzalność
To, co robisz dla jednej Private DNS Zone, możesz odtworzyć dla dziesięciu. Ta sama recipe i parametry zawsze dają ten sam efekt. Bez skryptów, bez ręcznych poprawek, bez ryzyka, że coś przeoczyłeś.
Na tym etapie migracja jest gotowa do uruchomienia przez taska rewriteRun i zatwierdzenia w standardowym procesie code review jako zwykły pull request, bez dodatkowych skryptów, wyjątków czy ręcznych poprawek.
Architektura rozwiązania
Powyższe rozwiązanie składa się z kilku jasno rozdzielonych elementów:
- Moje recipes w Javie.
- Recipes w YAML.
- Gradle.
Każdy z tych elementów pełni inną rolę w całym procesie migracji, a ich rozdzielenie pozwala zachować prostotę, testowalność i możliwość dalszego rozwoju.
Java recipes
Oficjalny katalog OpenRewrite nie zawiera recipes do zmiany wersji modułu, usuwania ani dodawania parametrów wejściowych w modułach OpenTofu / Terraform. Z tego powodu powstał zestaw własnych recipes, opublikowany w repozytorium paweloczadly/openrewrite-recipes.
Oprócz implementacji niezbędnych funkcji recipes zawierają solidny zestaw testów jednostkowych, które wprost weryfikują ich zachowanie i stanowią pierwszą linię zabezpieczenia przed niepożądanymi zmianami w strukturze HCL. Całość, w procesie CD, publikowana jest do Maven Central.
Ten element można traktować jako silnik refaktoryzacji, niezależny od konkretnych migracji czy projektów.
YAML recipes
Repozytorium infra-at-scale/avm-openrewrite-migrations zawiera recipes w YAML, które wywołują opisane wcześniej Java recipes. Na tym poziomie definiowane są już konkretne migracje dla modułów AVM w sposób jawny, deklaratywny i możliwy do wersjonowania.
Każda YAML recipe opisuje co ma zostać zmienione: docelową wersję modułu, usuwane parametry oraz nowe wymagane parametry, bez ingerencji w mechanikę samego refaktoringu. Dzięki temu logika migracji jest oddzielona od jej wykonania, a same recipes mogą być łatwo przeglądane, testowane i zatwierdzane w standardowym procesie code review.
W praktyce YAML recipes pełnią rolę warstwy decyzyjnej (“policy layer”). Pozwalają one opisać migrację jednego lub wielu projektów w sposób powtarzalny, deterministyczny i niezależny od konkretnego repozytorium czy struktury katalogów.
Gradle
Aby uczynić te migracje powtarzalnymi, wykorzystuję Gradle jako silnik wykonawczy. Odpowiada on za uruchamianie recipes OpenRewrite, zapewnia spójność wykonania i umożliwia łatwe odtworzenie tego samego procesu lokalnie oraz w CI.
W organizacjach, które już korzystają z Develocity, ten sam workflow może publikować Build Scan’y, co daje dodatkowe observability przebiegu migracji oraz możliwość porównywania wyników pomiędzy projektami, bez zmiany samego modelu wykonania.
Gwarancje bezpieczeństwa
Migracje infrastruktury jako kod są ryzykowne nie dlatego, że są trudne technicznie, ale dlatego, że często są niedeterministyczne, trudne do zweryfikowania i wykonywane ad-hoc. W przedstawionym podejściu bezpieczeństwo migracji nie wynika z ostrożności wykonawcy, lecz z właściwości samego procesu.
Poniżej opisuję, jakie gwarancje zapewnia zaproponowana architektura.
Deterministyczność zmian
Każda migracja jest opisana w postaci jawnej recipe YAML OpenRewrite. Oznacza to, że dla danego stanu wejściowego kod wyniku refaktoryzacji jest zawsze taki sam. Niezależnie od tego, kto i gdzie uruchamia ten proces. Nie ma tu miejsca na wyrażenia regularne ani kolejność dopasowań zależną od środowiska.
Deterministyczność jest kluczowa zwłaszcza w organizacjach, gdzie ta sama migracja musi zostać zastosowana w wielu repozytoriach lub środowiskach.
Idempotentność
Recipes OpenRewrite są idempotentne. Ich wielokrotne uruchomienie nie powoduje kumulowania zmian. Jeśli dana migracja została już zastosowana, kolejne wykonanie procesu nie wprowadzi dodatkowych modyfikacji.
Dzięki temu migracje mogą być:
- Bezpiecznie uruchamiane wielokrotnie.
- Integrowane z CI.
- Stosowane etapami, bez ryzyka zmian stanu infrastruktury.
Jawność i audyt
Zarówno Java recipes, jak i YAML recipes są przechowywane w repozytoriach Git. Cała logika migracji jest widoczna, wersjonowana i podlega standardowemu code review.
Nie ma tu:
- Ukrytych skryptów.
- Dynamicznie generowanych poleceń.
- Zmian wykonywanych “poza repozytorium”.
Dodatkowo, Java recipes są publikowane do Maven Central w automatycznym procesie CI/CD. Oznacza to, że artefakty są wersjonowane, podpisywane i możliwe do jednoznacznego zidentyfikowania w łańcuchu dostaw oprogramowania. Sam fakt publikacji nie jest gwarancją jakości, ale stanowi kolejny, audytowalny element procesu. Zarówno pod kątem pochodzenia kodu, jak i jego niezmienności po wydaniu.
Kontrola przed wdrożeniem
Możliwość uruchomienia migracji w trybie rewriteDryRun pozwala, tak jak w tofu plan / terraform plan, zobaczyć pełny zakres planowanych zmian. W tym przypadku bez modyfikowania kodu.
Daje to przestrzeń na:
- Ocenę wpływu migracji.
- Wychwycenie nieoczekiwanych efektów.
- Podjęcie świadomej decyzji o jej wdrożeniu.
Podsumowanie
Bezpieczeństwo przedstawionego podejścia nie opiera się na ręcznej kontroli ani ostrożności, lecz na właściwościach systemu: deterministyczności, idempotentności oraz jawności zmian. Dzięki temu migracje infrastruktury jako kod przestają być jednorazowym, ryzykownym wydarzeniem, a stają się powtarzalnym i kontrolowanym procesem.
Dlaczego to skaluje się w organizacji
Opisane podejście skaluje się w organizacji nie dlatego, że wykorzystuje konkretne narzędzia, ale dlatego, że adresuje problem na właściwym poziomie abstrakcji. Migracje nie są tu traktowane jako jednorazowe zadania wykonywane ręcznie w poszczególnych repozytoriach, lecz jako jawnie opisany proces, który można wielokrotnie zastosować w różnych kontekstach.
Kluczowe jest rozdzielenie odpowiedzialności:
- Java recipes definiują JAK wykonywać bezpieczny refaktoring HCL.
- YAML recipes opisują CO dokładnie ma zostać zmienione w danej migracji.
- Gradle zapewnia powtarzalny sposób uruchamiania całego procesu lokalnie oraz w CI.
Dzięki temu ta sama migracja może być zastosowana w wielu repozytoriach, uruchamiana przez różne zespoły, integrowana z istniejącymi procesami code review oraz CI/CD.
Co istotne, to podejście nie wymaga centralnego systemu, ani dedykowanej platformy. Wystarczają repozytoria Git oraz standardowy workflow pull requestów. To wszystko sprawia, że rozwiązanie dobrze wpisuje się w realia organizacji o różnym poziomie dojrzałości.
Kiedy to podejście nie ma sensu
Opisane podejście nie będzie dobrym rozwiązaniem w sytuacji, gdy projekt infrastruktury jako kod jest niewielki, obejmuje pojedynczy moduł i nie planuje się jego dalszego rozwoju ani aktualizacji. W tym przypadku koszt wprowadzenia dodatkowego procesu może przewyższyć potencjalne korzyści.
Podobnie, w sytuacjach wymagających jednorazowej, standardowej zmiany (np. tylko podbicia wersji w jednym miejscu), modyfikacja przez Renovate / inną automatyzację lub nawet ręczna może być prostszym i bardziej adekwatnym rozwiązaniem.
Warto też podkreślić, że podejście to zakłada pewien poziom dojrzałości organizacyjnej: pracę z pull requestami, code review oraz gotowość do traktowania migracji jako elementu długofalowego utrzymania, a nie incydentalnej poprawki.
Podsumowanie
Migracje infrastruktury jako kod są nieuniknione. Zmieniają się wersje modułów, wymagania dostawców chmurowych oraz dobre praktyki. Problemem nie jest sama zmiana, lecz sposób jej przeprowadzania.
W artykule pokazałem, jak podejść do refaktoryzacji OpenTofu / Terraform w sposób deterministyczny, powtarzalny i bezpieczny, wykorzystując OpenRewrite jako silnik refaktoryzacji, a Gradle jako mechanizm uruchomieniowy. Zamiast skryptów opartych na grep i sed, które są trudne do utrzymania i weryfikacji, zaproponowane podejście opiera się na jawnych recipes, minimalnych diffach i pull requestach jako jedynym wyniku procesu.
Dzięki temu migracje przestają być ryzykownym, jednorazowym wydarzeniem, a stają się kontrolowanym procesem inżynierskim, który można rozwijać, audytować i bezpiecznie stosować w całej organizacji.
Co istotne, zaprezentowany model refaktoryzacji nie jest związany z jednym formatem ani jednym narzędziem. Jawne recipes, deterministyczne wykonanie i pull request jako wynik tworzą uniwersalne podejście do bezpiecznej ewolucji konfiguracji infrastrukturalnych.
Jeśli pracujesz z AVM i zastanawiasz się, jak bezpiecznie podejść do migracji, to podejście możesz przetestować w praktyce.
👉 Zajrzyj do repozytorium infra-at-scale/avm-openrewrite-migrations i uruchom migracje na swoich projektach.
Chcesz dodać nowe funkcjonalności albo naprawić błąd w recipes?
👉 Zrób fork paweloczadly/openrewrite-recipes i wprowadź potrzebne zmiany.
A jeśli spotykasz się ze skomplikowanymi scenariuszami migracji w ekosystemie OpenTofu / Terraform lub w konfiguracjach Kubernetes (Helm, Flux, Argo CD), albo chcesz przedyskutować zastosowanie tego podejścia w Twojej organizacji:
👉 skontaktuj się ze mną pod contact@oczadly.io.
