W LinuxLab AWX traktujemy jako usługę produkcyjną, a nie jako „Ansible z panelem WWW”. Ma własną bazę, własne sekrety, własny cykl aktualizacji i własny sposób uruchamiania zadań. Dlatego zanim ktokolwiek dostanie do niego dostęp, przechodzimy przez wersje, sekrety, szyfrowanie połączenia protokołem TLS (od angielskiego Transport Layer Security) i jedno zadanie, które potwierdza działanie całego łańcucha. Wszystkie liczby i wyniki w tym artykule pochodzą z instancji AWX, która działa w naszym labie i jest przez nas realnie używana - to nie jest maszyna postawiona na potrzeby tekstu i skasowana po jego napisaniu.
W tym artykule instalujemy pojedynczą instancję AWX na jednej maszynie wirtualnej z k3s. Wariant główny używa domyślnego Traefika, usługi widocznej tylko wewnątrz klastra, własnego certyfikatu i nazwy DNS. Po drodze omijamy obraz wycofany z rejestru, sprawdzamy manifest przed wdrożeniem, zamykamy otwarte HTTP i uruchamiamy pierwszy job. Wszystkie nazwy hostów, organizacji i adresy w przykładach są umowne. Przy każdym kroku zaznaczamy, co zostało zmierzone na maszynie testowej, a co jest zaleceniem opartym na dokumentacji.
Spis treści
- Co sprawdziliśmy, a czego nie
- Krótki słownik
- Jak wygląda przepływ
- Decyzje przed instalacją
- Maszyna wirtualna krok po kroku
- Instalacja k3s
- Wersje i martwy obraz
- Sekrety, SECRET_KEY i TLS
- Manifest AWX
- Ingress, TLS i otwarte HTTP
- Pierwsze wejście do AWX
- Skąd bierze się
latest - Pierwszy projekt i pierwszy job
- Model obiektów bez mieszania pojęć
- Diagnostyka: od objawu do dowodu
- Backup, restore i upgrade
- Ograniczenia tego podejścia
- Checklista przed udostępnieniem
Co sprawdziliśmy, a czego nie
AWX daje Ansible'owi interfejs WWW i API, kontrolę dostępu, historię zadań, harmonogramy oraz bezpieczniejsze przechowywanie poświadczeń. Nie jest jednak Ansiblem zainstalowanym na serwerze. Zadanie wykonuje się w execution environment, czyli w kontenerze, a nie w Pythonie ani w ansible-core zainstalowanym na maszynie z k3s. To rozróżnienie decyduje później o tym, gdzie muszą znaleźć się klucze SSH i kolekcje.
Poniższa tabela opisuje stan naszej labowej instancji w dniu publikacji. Wszystkie wartości pochodzą z odczytu działającej instalacji, a nie z przepisania cudzej konfiguracji. Lab siedzi w sieci prywatnej i jest to wybór celowy, a nie niedoróbka: maszyna nie ma publicznego DNS ani ruchu z Internetu do portów 80 i 443. Ma to jedną konsekwencję dla tego artykułu, którą opisujemy niżej wprost - nie dało się na niej domknąć testu certyfikatu z Let's Encrypt.
| Element | Stan zmierzony 10 września 2026 |
|---|---|
| Maszyna | Ubuntu 24.04.4 LTS, jądro 6.8, 4 vCPU, 7,7 GiB RAM, dysk 38 GiB użytkowego, 26 GiB wolnego |
| Kubernetes | jednowęzłowy k3s v1.34.3+k3s3, node w stanie Ready, runtime containerd 2.1.5 |
| Storage i ingress | domyślna StorageClass local-path, IngressClass o nazwie traefik |
| AWX | AWX Operator 2.19.1, AWX 24.6.1, baza z obrazu quay.io/sclorg/postgresql-15-c9s |
| Pody | operator, web, task i PostgreSQL w stanie Running; dysk bazy przydzielony i podłączony (stan Bound), 8 GiB, klasa local-path |
| API | /api/v2/ping/ zwróciło HTTP 200 i wersję 24.6.1 |
| Obraz proxy | w podzie operatora działa quay.io/brancz/kube-rbac-proxy:v0.15.0, nie obraz z gcr.io |
| Manifest operatora | polecenie kubectl kustomize (Kustomize 5.7.1 wbudowany w kubectl 1.34.3) wygenerowało manifest zawierający oba oczekiwane obrazy |
| Manifest AWX | pokazany niżej zasób przeszedł kubectl apply --dry-run=server wobec działającego CRD (od angielskiego Custom Resource Definition, definicja własnego typu zasobu - opisujemy to niżej) operatora 2.19.1 |
| Ingress i TLS | Ingress klasy traefik z sekretem TLS: HTTPS zwróciło 200 i odpowiedź AWX; nieznany Host zwrócił 404 z Traefika |
| Wykonanie automatyzacji | pojedyncze zadanie sprawdzające (moduł ping), wykonane na samej maszynie AWX bez połączenia po sieci, zakończyło się stanem successful po 85,0 s - w tym pierwsze pobranie obrazu środowiska wykonawczego |
Równie ważne jest to, czego nie potwierdziliśmy, i co w tym artykule pozostaje zaleceniem, a nie zakończonym testem:
- Wydanie certyfikatu Let's Encrypt. Jak wyżej: lab celowo stoi w sieci prywatnej, bez publicznego DNS i bez ruchu z Internetu do portów 80 i 443, więc weryfikacja HTTP-01 nie miałaby jak przejść - polega ona na tym, że urząd certyfikacji sam łączy się z Twoją domeną po zwykłym HTTP i pobiera z niej plik, którym dowodzisz, że domena jest Twoja. Sekcja o TLS pokazuje poprawne miejsce na sekret z certyfikatem, a nie zakończony test ACME - z angielskiego Automatic Certificate Management Environment, czyli protokołu, którym Certbot i cert-manager automatycznie wystawiają i odnawiają certyfikaty. Sam Ingress sprawdziliśmy certyfikatem wystawionym lokalnie.
- Instalacja od zera jednym przebiegiem. Sprawdzana instancja już działała, więc kroki instalacyjne potwierdziliśmy tak, jak się je realnie potwierdza przed wdrożeniem: renderem manifestu operatora i serwerowym
--dry-runzasobu AWX wobec żywego CRD. - Połączenie SSH z execution environmentu do rzeczywistych hostów. Zadanie testowe wykonało się na tej samej maszynie, na której działa AWX, i to bez logowania się dokądkolwiek: Ansible uruchomił moduł bezpośrednio w kontenerze, w którym już był, zamiast łączyć się przez sieć. Dowodzi to, że AWX przyjął zadanie, utworzył dla niego kontener i wykonał w nim kod. Nie dowodzi natomiast, że zadziała Twój klucz SSH, Twoja sieć ani uprawnienia na docelowym serwerze - żaden z tych elementów nie był w tym teście w ogóle użyty.
- Backup i odtworzenie zasobami
AWXBackup/AWXRestore. Opisujemy procedurę i jej pułapki, ale w tym materiale nie pokazujemy przebiegu odtworzenia.
To jest materiał dla pojedynczej maszyny, nie przepis na wysoką dostępność. local-path w k3s jest storage'em lokalnym dla konkretnego węzła. Utrata tej maszyny nie jest scenariuszem, który rozwiąże sam Kubernetes: konieczny jest backup poza nią i regularnie testowane odtworzenie. Konfiguracja 4 vCPU i 8 GiB RAM jest punktem startu, a nie obietnicą pojemności - rzeczywiste zapotrzebowanie zależy od liczby równoległych zadań, rozmiaru execution environmentów, synchronizacji projektów i liczby hostów zbierających fakty.
Krótki słownik
- AWX - aplikacja z interfejsem WWW, API i silnikiem zadań, zbudowana na Ansible. Składa się między innymi z podów
webitaskoraz bazy PostgreSQL. To otwarty projekt społecznościowy sponsorowany przez Red Hat, na licencji Apache 2.0, i jednocześnie upstream komponentu automation controller - czyli tego, co wcześniej nosiło nazwę Ansible Tower - w komercyjnym Red Hat Ansible Automation Platform. W praktyce jest to darmowy odpowiednik płatnego produktu, ale nie jego klon: zależność biegnie w drugą stronę, bo to automation controller powstaje z wybranych wydań AWX, a nie odwrotnie. - AWX Operator - kontroler działający w klastrze, który na podstawie zasobu
AWXtworzy i utrzymuje wszystkie obiekty aplikacji. Ma własny cykl wydań, inny niż AWX. - CRD - z angielskiego Custom Resource Definition, czyli definicja własnego typu zasobu. Kubernetes zna standardowo pody czy usługi; CRD dokłada do jego API zupełnie nowy rodzaj obiektu, o którym sam Kubernetes wcześniej nie wiedział. Operator dodaje w ten sposób typy
AWX,AWXBackupiAWXRestore. - CR - z angielskiego Custom Resource, czyli pojedynczy obiekt takiego nowego typu. Relacja jest ta sama, co między typem a jego egzemplarzem: CRD mówi, że typ
AWXw ogóle istnieje, a CR to już konkretny AWX o konkretnej nazwie. Plikawx.yamlz tego artykułu jest właśnie CR-em typuAWX. - Pod - najmniejsza jednostka uruchamiana przez Kubernetes: jeden lub kilka kontenerów, które startują razem, dzielą adres sieciowy i są traktowane jak całość. Pody nie są trwałe - kasuje się je i tworzy od nowa, i to jest normalny tryb pracy, a nie awaria.
- Namespace - przestrzeń nazw, czyli wydzielona część klastra z własnym zestawem obiektów. Jeden klaster może mieć osobną przestrzeń dla aplikacji A i osobną dla B; nazwy obiektów obowiązują w obrębie przestrzeni, więc w każdej z nich może istnieć usługa o tej samej nazwie i nie będą sobie przeszkadzać. Wszystko, co robimy w tym artykule, dzieje się w przestrzeni
awx, stąd-n awxw prawie każdym poleceniu. - PVC - z angielskiego PersistentVolumeClaim, czyli zgłoszenie zapotrzebowania na trwałe miejsce na dysku. Pod sam w sobie nic nie zapamiętuje, więc baza PostgreSQL dostaje PVC i to na nim przeżywają dane, gdy pod zostanie odtworzony.
- Inventory - lista hostów, którymi Ansible ma zarządzać, wraz z ich grupami i zmiennymi. W AWX jest osobnym obiektem, a nie plikiem w repozytorium z playbookami.
- EE - z angielskiego execution environment, czyli środowisko wykonawcze: obraz kontenera z
ansible-core, kolekcjami i zależnościami. Każde zadanie dostaje własny, osobny pod zbudowany z tego obrazu, o nazwieautomation-job-<numer zadania>-<losowy sufiks>, i pod ten jest usuwany zaraz po zakończeniu zadania. Sprawdziliśmy to, uruchamiając dwa zadania jednocześnie: powstały dwa niezależne pody, po jednym na zadanie. - Job template - zapisane, powtarzalne połączenie projektu, inventory, poświadczeń i EE. Uruchamiane z interfejsu, API albo harmonogramu.
SECRET_KEY- klucz, którym AWX szyfruje między innymi poświadczenia zapisane w bazie. Baza odtworzona z innym kluczem nie da się odszyfrować.- Ingress - obiekt Kubernetesa opisujący, jak ruch HTTP/HTTPS spoza klastra trafia do usługi. W k3s obsługuje go domyślnie Traefik.
Darmowy nie znaczy wspierany. Automation controller powstaje tak, że Red Hat bierze wybrane wydania AWX i dopracowuje je pod kątem długoterminowego wsparcia - to ten sam model, co Fedora i Red Hat Enterprise Linux. AWX idzie natomiast szybko: nowe buildy mniej więcej co dwa tygodnie, wsparcie wyłącznie społecznościowe, bez płatnego wsparcia Red Hata. Etykieta „stable” nie oznacza przy tym przydatności produkcyjnej ani żadnej gwarancji, a na pytanie „czy Red Hat rekomenduje AWX na produkcję”, FAQ projektu odpowiada jednym słowem: nie. To nie znaczy, że AWX na produkcji nie działa - znaczy, że ryzyko utrzymania, aktualizacji i odtworzenia bierzesz na siebie. Cała reszta tego artykułu jest właśnie o tym, jak to ryzyko ograniczyć.
Jak wygląda przepływ
Zanim pojawi się pierwszy manifest, warto zobaczyć, którędy idzie ruch użytkownika, a którędy ruch automatyzacji. To dwie różne ścieżki i mylenie ich jest źródłem większości nieporozumień z AWX.
przeglądarka / API
|
| HTTPS, DNS: awx.example.org
v
Traefik Ingress w k3s
|
v
AWX web <----> PostgreSQL
|
v
AWX task / receptor ----> pod z execution environment
|
| SSH / API / WinRM
v
hosty zarządzane
Dwa elementy z tego schematu warto nazwać po imieniu. AWX web to część, którą widzisz w przeglądarce i która odpowiada na zapytania API. AWX task to część, która pilnuje kolejki zadań i decyduje, co i kiedy uruchomić; receptor jest jej warstwą sieciową, odpowiedzialną za dostarczenie zadania tam, gdzie ma się wykonać - przy jednej maszynie po prostu do kontenera obok. Zadanie nie wykonuje się więc ani w przeglądarce, ani w podzie web, tylko w osobnym, tworzonym na tę okazję kontenerze.
Najważniejszy wniosek z tego diagramu: prywatny klucz SSH, kolekcje Ansible i binarki potrzebne zadaniu muszą być dostępne dla AWX oraz dla jego execution environmentu. Wrzucenie ich do ~/.ssh użytkownika na maszynie z k3s albo doinstalowanie pakietu do systemowego Pythona tej maszyny nie rozwiązuje problemu, bo zadanie w ogóle tam nie zagląda.
Decyzje przed instalacją
Cztery pytania, na które warto odpowiedzieć przed uruchomieniem pierwszego manifestu. Każde z nich zmienia kształt instalacji, a nie tylko jej kosmetykę.
- Lab czy usługa dla zespołu? Lab może korzystać z
port-forward. Usługa zespołowa potrzebuje HTTPS, nazwy DNS i kontroli dostępu od pierwszego dnia, bo później dochodzą do niej konta i poświadczenia. - Gdzie będzie kopia zapasowa? Nie na tym samym dysku i nie tylko jako zrzut PostgreSQL. Bez sekretów Kubernetes taki zrzut jest niepełny.
- Czy masz publiczne DNS i otwarte porty 80/443 dla ACME? Jeżeli nie, użyj certyfikatu z własnej infrastruktury kluczy (PKI, z angielskiego Public Key Infrastructure) albo nie wystawiaj usługi poza sieć prywatną.
- Jaka jest granica odpowiedzialności zadań? Pierwszy template ma dotykać dedykowanego hosta testowego, a nie całej produkcyjnej inventory. Ta decyzja zapada przed instalacją, bo potem trudniej ją cofnąć.
Maszyna wirtualna krok po kroku
Zanim pojawi się pierwsze polecenie k3s, musi istnieć maszyna, na której to wszystko stanie. Ta sekcja prowadzi od pustej maszyny do momentu, w którym można zacząć instalację. Cała droga do działającego AWX ma osiem kroków i wygląda tak:
- Przygotuj maszynę wirtualną - procesor, pamięć, dysk, system i dostęp SSH.
- Sprawdź jej gotowość - czas, uprawnienia i wyjście na zewnątrz. Dowód: cztery polecenia z tej sekcji.
- Zainstaluj k3s. Dowód: jeden węzeł w stanie
Ready. - Wygeneruj manifest operatora i sprawdź obrazy. Dowód: dwie linie z
grep. - Utwórz sekrety - hasło administratora i
SECRET_KEY. - Wdróż operator, potem zasób AWX. Dowód: pody
web,taski baza w stanieRunning. - Wystaw usługę przez HTTPS. Dowód: HTTP 200 z
/api/v2/ping/i przekierowanie z portu 80. - Uruchom pierwsze zadanie. Dowód: stan
successfulna dedykowanym hoście testowym.
Kroki od trzeciego wzwyż opisują kolejne sekcje. Tutaj zajmujemy się dwoma pierwszymi.
Ile maszyna musi mieć
Warto rozdzielić dwie różne liczby, bo bardzo łatwo je pomylić. Dokumentacja k3s podaje minimum dla samego Kubernetesa, a nie dla aplikacji, które w nim uruchomisz. AWX potrzebuje wyraźnie więcej niż to minimum, więc poniżej zestawiamy jedno z drugim.
| Zasób | Minimum k3s wg dokumentacji | Użyte dla AWX | Dlaczego tyle |
|---|---|---|---|
| Procesor | 2 rdzenie (węzeł serwera) | 4 rdzenie | AWX to kilkanaście kontenerów naraz, a każde zadanie dostaje jeszcze własny. Na 4 rdzeniach AWX wyliczył sobie pojemność 16 procesów Ansible. |
| Pamięć | 2 GB (węzeł serwera) | 8 GiB | Sam AWX z bazą i Redisem zajmuje kilka GiB w spoczynku. Na maszynie testowej przy bezczynnym AWX zajęte było 3,3 GiB. |
| Dysk | zalecany SSD | 40 GiB | Same obrazy kontenerów to blisko 2 GiB, do tego 8 GiB na dane bazy i miejsce na obrazy środowisk wykonawczych, które dochodzą z czasem. Po instalacji i pierwszym zadaniu zajęte było 12 z 38 GiB. |
| System | - | Ubuntu 24.04 LTS | Wersja, na której prowadzono wszystkie testy z tego artykułu. Debian działa tak samo; różnice dotyczą nazw pakietów, nie k3s. |
Maszyna potrzebuje też stałego adresu IP w sieci, z której będziesz się z nią łączyć, oraz konta z prawem do sudo. Jeżeli AWX ma być dostępny pod nazwą DNS i po HTTPS, ta nazwa musi wskazywać właśnie na ten adres - ale rekord DNS można dodać później, bo do samej instalacji nie jest potrzebny.
Skąd wziąć maszynę
Może pochodzić z dowolnego źródła: od dostawcy hostingu, z Proxmoxa, z VMware, z VirtualBoxa albo z chmury. Dla AWX nie ma to znaczenia, dopóki spełnia tabelę wyżej. Jeżeli tworzysz ją samodzielnie na Linuksie z KVM - z angielskiego Kernel-based Virtual Machine, czyli wirtualizacją wbudowaną w jądro Linuksa - poniższy zestaw poleceń buduje ją z gotowego obrazu chmurowego, bez przechodzenia przez instalator systemu.
Najpierw plik konfiguracyjny cloud-init, czyli mechanizmu, który przy pierwszym starcie ustawia w obrazie chmurowym nazwę hosta, konto i klucz SSH. Bez niego obraz wstanie, ale nie będzie jak się do niego zalogować. Wstaw swój własny klucz publiczny:
cat > user-data <<'EOF'
#cloud-config
hostname: awx
users:
- name: awxadm
groups: [sudo]
shell: /bin/bash
sudo: ['ALL=(ALL) NOPASSWD:ALL']
ssh_authorized_keys:
- ssh-ed25519 AAAA... twoj-klucz-publiczny
package_update: true
EOF
printf 'instance-id: awx-01\nlocal-hostname: awx\n' > meta-data
# Oba pliki trafiają na maleńki obraz ISO o etykiecie cidata - stąd cloud-init je czyta.
genisoimage -output seed.iso -volid cidata -joliet -rock user-data meta-data
Potem dysk i sama maszyna. Obraz chmurowy Ubuntu pobierz ze strony cloud-images.ubuntu.com - plik dla KVM to wariant amd64.img:
# Obraz chmurowy jest mały; rozszerzamy go do docelowego rozmiaru dysku.
qemu-img create -f qcow2 -F qcow2 -b noble-server-cloudimg-amd64.img awx.qcow2 40G
virt-install --name awx --memory 8192 --vcpus 4 --cpu host-passthrough \
--disk path="$PWD/awx.qcow2",format=qcow2,bus=virtio \
--disk path="$PWD/seed.iso",device=cdrom \
--os-variant ubuntu24.04 --network network=default,model=virtio \
--graphics none --import --noautoconsole
Zanim uruchomisz to naprawdę, dopisz na końcu --dry-run. Polecenie sprawdzi wtedy poprawność wszystkich parametrów i nic nie utworzy - tak samo jak --dry-run=server przy manifeście AWX w dalszej części artykułu. Adres maszyny pokaże potem virsh domifaddr awx.
Kontrola gotowości przed instalacją k3s
Cztery sprawdzenia na świeżej maszynie. Zajmują minutę, a oszczędzają szukania przyczyny w połowie instalacji:
# 1. System i zasoby - czy maszyna jest tym, czym miała być
grep PRETTY_NAME /etc/os-release
nproc; free -h; df -h /
# 2. Czas - rozjechany zegar psuje certyfikaty i uwierzytelnianie
timedatectl | grep -E 'System clock|NTP service'
# 3. Uprawnienia - instalacja k3s wymaga roota
sudo -v
# 4. Wyjście na zewnątrz - instalator i obrazy pobierają się z sieci
curl -sS -o /dev/null -w 'k3s: %{http_code}\n' https://get.k3s.io
curl -sS -o /dev/null -w 'quay: %{http_code}\n' https://quay.io/v2/
curl -sS -o /dev/null -w 'github: %{http_code}\n' https://github.com
Oczekiwany wynik to zsynchronizowany zegar z aktywną usługą synchronizacji czasu NTP (od angielskiego Network Time Protocol) oraz kody 200 dla get.k3s.io i GitHuba. W przypadku quay.io poprawną odpowiedzią jest 401, a nie 200: pytamy o punkt wejścia rejestru bez logowania, więc odmowa dostępu jest tu dowodem, że rejestr odpowiada. Dostęp do GitHuba jest potrzebny, bo manifest operatora pobiera się właśnie stamtąd, a obrazy - z quay.io.
Na maszynie testowej wynikiem było: Ubuntu 24.04.4 LTS, 4 rdzenie, 7,7 GiB pamięci, 26 GiB wolnego dysku, zegar zsynchronizowany i wszystkie trzy adresy osiągalne.
Instalacja k3s
Maszyna jest gotowa, więc instalujemy na niej Kubernetesa. Przed instalacją warto przejrzeć wymagania k3s, zwłaszcza porty i zasady sieciowe. Nie wyłączaj Traefika przełącznikiem --disable traefik, jeżeli chcesz użyć ścieżki Ingress opisanej dalej.
sudo apt update
sudo apt install -y ca-certificates curl openssl
# Wersja przetestowana w tym materiale. Nie zastępuj jej bez testu upgrade'u.
K3S_VERSION='v1.34.3+k3s3'
curl -sfL https://get.k3s.io | \
sudo env INSTALL_K3S_VERSION="$K3S_VERSION" sh -s - server
sudo systemctl enable --now k3s
sudo k3s kubectl get nodes -o wide
sudo k3s kubectl get storageclass
sudo k3s kubectl get ingressclass
Oczekiwane minimum to jeden node w stanie Ready, domyślna StorageClass local-path oraz klasa Ingress obsługiwana przez Traefika. W sprawdzanej instalacji kubectl get ingressclass zwróciło dokładnie jedną pozycję o nazwie traefik z kontrolerem traefik.io/ingress-controller. Jeżeli Twój klaster zwróci inną klasę, nie wpisuj w ciemno traefik do manifestu AWX - użyj faktycznie dostępnej nazwy albo pomiń to pole zgodnie z konfiguracją swojego kontrolera.
Do codziennej pracy wygodniejszy jest kubeconfig użytkownika niż każdorazowe sudo k3s kubectl. Nie kopiuj tego pliku na publiczny host ani do repozytorium:
mkdir -p ~/.kube
sudo install -m 0600 -o "$USER" -g "$USER" \
/etc/rancher/k3s/k3s.yaml ~/.kube/config
kubectl get nodes
kubectl -n kube-system get pods
Co oznacza local-path
Local Path Provisioner tworzy wolumen na dysku węzła. Dla jednego węzła jest to wygodne: operator AWX może utworzyć PVC dla PostgreSQL bez osobnego serwera NFS ani sterownika CSI - z angielskiego Container Storage Interface, czyli standardowego sposobu podłączania zewnętrznych macierzy do Kubernetesa. Na maszynie testowej PVC bazy ma 8 GiB, klasę local-path i stan Bound. Nie jest to jednak wolumen przenośny między węzłami ani replikowana macierz, a tryb wiązania WaitForFirstConsumer oznacza, że wolumen powstaje dopiero wtedy, gdy pojawi się pod, który go używa.
Przy local-path operatorowi trzeba pozwolić przygotować katalog danych PostgreSQL z właściwym identyfikatorem użytkownika. Dlatego w manifeście niżej pojawia się postgres_data_volume_init: true; opis tego pola w CRD mówi wprost, że ustawia ono uprawnienia katalogu /var/lib/pgdata/data przez kontener inicjalizujący.
Wersje: zadeklaruj je i sprawdź przed wdrożeniem
Zadeklarowanie wersji to wpisanie dokładnego numeru tam, gdzie kusi ogólna etykieta. Obrazy kontenerów pobiera się po tagu, czyli etykiecie wskazującej konkretną zawartość w rejestrze: tagiem jest zarówno 24.6.1, jak i latest. Różnica między nimi jest zasadnicza. latest znaczy „to, co akurat jest w rejestrze najnowsze”, więc to samo polecenie uruchomione dziś i za miesiąc może pobrać dwa różne obrazy, a Ty się o tym nie dowiesz. Tag 24.6.1 znaczy zawsze to samo. Wpisanie dokładnego numeru to świadoma rezygnacja z automatycznej nowości na rzecz powtarzalności - i tego, że awarię da się później odtworzyć.
Przy AWX ma to znaczenie podwójne, bo składają się na niego dwa osobno wydawane elementy: operator i sama aplikacja. Deklarujemy więc obie wersje, a nie jedną.
W momencie testu najnowszym wydaniem AWX Operatora było 2.19.1, opublikowane razem z AWX 24.6.1. To nie jest deklaracja oficjalnego wsparcia dowolnej kombinacji z dowolną wersją k3s; to konkretna para uruchomiona i sprawdzona na k3s v1.34.3+k3s3.
Pułapka operatora 2.19.1. Jego upstreamowy manifest wskazuje obraz gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0, a ten obraz został wycofany. W dniu testu zapytanie o manifest tego tagu zwróciło HTTP 404 z komunikatem MANIFEST_UNKNOWN, a lista tagów całego repozytorium była pusta - to nie jest kwestia jednej brakującej wersji. Wycofanie obrazu i jego zamienniki opisuje dyskusja w projekcie Kubebuilder. Bez podmiany obrazu pod operatora utknie w ImagePullBackOff.
Działająca instalacja używa zamiast niego quay.io/brancz/kube-rbac-proxy:v0.15.0, czyli tej samej wersji z macierzystego repozytorium projektu. To świadomy, przetestowany workaround, a nie niewidoczna zmiana obrazu: podmianę zapisujemy jawnie w Kustomize, żeby była widoczna w przeglądzie zmian. Przed każdym przyszłym upgrade'em ponownie sprawdź render i obrazy - i nie przestawiaj tagów na latest.
Utwórz katalog roboczy poza repozytorium z playbookami albo dodaj go do prywatnego repozytorium infrastruktury. Poniższe pliki nie zawierają sekretów.
mkdir -p ~/awx-k3s/operator
cd ~/awx-k3s
Plik operator/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: awx
resources:
- github.com/ansible/awx-operator/config/default?ref=2.19.1
images:
- name: quay.io/ansible/awx-operator
newTag: 2.19.1
# gcr.io/kubebuilder/kube-rbac-proxy zostal wycofany z rejestru.
# quay.io/brancz to macierzyste repozytorium projektu - ta sama wersja.
- name: gcr.io/kubebuilder/kube-rbac-proxy
newName: quay.io/brancz/kube-rbac-proxy
newTag: v0.15.0
Najpierw wyrenderuj manifest. To łapie literówki i potwierdza, że obejście martwego obrazu faktycznie weszło do wyniku:
kubectl kustomize operator | \
grep -E 'quay.io/ansible/awx-operator:2\.19\.1|quay.io/brancz/kube-rbac-proxy:v0\.15\.0'
Poprawny wynik to dwie linie, po jednej na każdy obraz. Jeżeli grep nie zwróci nic, nie wykonuj kubectl apply: sprawdź wersję Kustomize, zawartość pliku i dostęp do GitHuba. Render pobiera bazę zdalnie, więc maszyna musi mieć wyjście na zewnątrz.
Warto też wiedzieć, co dokładnie ten render tworzy. Poza czterema definicjami CRD i wdrożeniem kontrolera zawiera on również obiekt Namespace. Namespace utworzony niżej ręcznie nie jest więc konfliktem - potrzebujemy go wcześniej tylko po to, żeby sekrety zdążyły powstać przed operatorem.
Sekrety administratora, SECRET_KEY i TLS
AWX potrafi sam wygenerować hasło administratora i SECRET_KEY, ale dla powtarzalnego wdrożenia lepiej utworzyć oba jawnie jako obiekty Secret. Powód jest praktyczny: SECRET_KEY szyfruje między innymi poświadczenia zapisane w bazie, a odtworzenie samej bazy z nowym kluczem kończy się tym, że tych danych nie da się odszyfrować. Backup bazy bez tego klucza jest backupem niepełnym.
Wygeneruj wartości lokalnie, na zaufanym terminalu. Nie umieszczaj ich w YAML-u, repozytorium, historii czatu ani w logu systemu ciągłej integracji, w skrócie CI (od angielskiego Continuous Integration):
kubectl create namespace awx
kubectl -n awx create secret generic awx-admin-password \
--from-literal=password="$(openssl rand -base64 32)"
kubectl -n awx create secret generic awx-secret-key \
--from-literal=secret_key="$(openssl rand -base64 48)"
Do Ingressu potrzebny jest osobny sekret typu tls. Jeżeli certyfikat i klucz już istnieją, na przykład z firmowej infrastruktury kluczy albo z cert-managera, utwórz go tak:
kubectl -n awx create secret tls awx-tls \
--cert=/bezpieczna/sciezka/fullchain.pem \
--key=/bezpieczna/sciezka/privkey.pem
Cert-manager jest dobrym sposobem automatycznego odnawiania, ale zanim zadziała wyzwanie HTTP-01, muszą działać publiczny DNS, port 80 i routing do Traefika. W środowisku bez tych trzech elementów lepiej użyć certyfikatu z własnej infrastruktury i nie udawać, że ACME zostało skonfigurowane.
Jest jeszcze jedno ustawienie, o którym warto wiedzieć, zanim ktoś zacznie sprzątać klaster. Pole garbage_collect_secrets ma w CRD wartość domyślną false, co znaczy, że usunięcie zasobu AWX nie kasuje jego sekretów. Ta wartość domyślna działa na Twoją korzyść - SECRET_KEY przeżywa skasowanie instancji. Nie przestawiaj jej na true, dopóki nie masz sprawdzonego backupu tych sekretów poza klastrem.
Manifest AWX
Zapisz plik awx.yaml. Zmień awx.example.org na własną nazwę DNS zanim utworzysz rekord i certyfikat. Poniższy manifest przeszedł serwerowy --dry-run wobec CRD operatora 2.19.1.
apiVersion: awx.ansible.com/v1beta1
kind: AWX
metadata:
name: awx
namespace: awx
spec:
# Zadeklarowana, sprawdzona para operator/AWX.
image_version: 24.6.1
admin_user: admin
admin_password_secret: awx-admin-password
secret_key_secret: awx-secret-key
# Wystawienie przez Traefik, nie przez publiczny NodePort.
service_type: ClusterIP
ingress_type: ingress
ingress_class_name: traefik
ingress_hosts:
- hostname: awx.example.org
tls_secret: awx-tls
# Jednowęzłowy k3s: trwałość na lokalnym dysku tej maszyny.
postgres_storage_class: local-path
postgres_storage_requirements:
requests:
storage: 8Gi
postgres_data_volume_init: true
# Domyślnie true: upgrade operatora pociąga za sobą upgrade instancji.
auto_upgrade: false
Kilka uwag do tych pól, sprawdzonych w schemacie CRD zainstalowanego operatora:
service_typeprzyjmuje wyłącznieLoadBalancer,ClusterIPlubNodePort(także pisane małymi literami). Inna wartość zostanie odrzucona przez API, a nie po cichu zignorowana.ingress_typeprzyjmujenone,Ingress,ingress,Routelubroute. WariantRoutedotyczy OpenShiftu, nie k3s.ingress_hostsjest listą obiektów z polamihostnameitls_secret. Starsze polahostnameiingress_tls_secretsą w tym CRD oznaczone jako przestarzałe - nie mieszaj obu zapisów.auto_upgradema domyślnie wartośćtrue, a jego opis w CRD brzmi: instancje AWX mają być automatycznie aktualizowane, gdy zaktualizowany zostanie operator. To znaczy, że podbicie operatora nie jest odizolowanym krokiem. Jeżeli chcesz rozdzielić te dwie decyzje, ustawfalsejawnie i aktualizuj aplikację osobno.
Jeżeli wcześniejsze kubectl get ingressclass nie pokazało klasy traefik, zamień ingress_class_name na nazwę własnej klasy albo usuń tę linię. Nie usuwaj natomiast tls_secret, dopóki nie zdecydujesz świadomie, że tymczasowo dopuszczasz zwykłe HTTP.
Zanim cokolwiek wdrożysz, sprawdź manifest przy udziale serwera API. To nie jest to samo co walidacja składni YAML: --dry-run=server konfrontuje plik z rzeczywistym CRD w klastrze, więc wyłapie pole, którego dana wersja operatora nie zna.
kubectl apply -f awx.yaml --dry-run=server
Kolejność wdrożenia ma znaczenie. Nie mieszaj pierwszego wdrożenia operatora i zasobu AWX w jednym apply -k: najpierw CRD i kontroler, potem zasób AWX. Unikniesz w ten sposób wyścigu, w którym serwer API nie zna jeszcze typu AWX.
cd ~/awx-k3s
kubectl apply -k operator
kubectl wait --for=condition=Established \
crd/awxs.awx.ansible.com --timeout=2m
kubectl -n awx rollout status deployment/awx-operator-controller-manager \
--timeout=5m
kubectl apply -f awx.yaml
kubectl -n awx get awx,pods,svc,pvc,ingress
Pierwsze pobranie obrazów może potrwać kilka minut. Patrz na stan, a nie na zgadywany czas:
kubectl -n awx get pods -w
# Gdy coś utknie:
kubectl -n awx get events --sort-by=.lastTimestamp
kubectl -n awx logs deployment/awx-operator-controller-manager \
-c awx-manager --tail=100
Prawidłowy koniec tego etapu to gotowe pody operatora, awx-web, awx-task i PostgreSQL oraz PVC bazy w stanie Bound. Warto zwrócić uwagę na dwie rzeczy. Po pierwsze, wszystkie obiekty aplikacji dostają nazwę pochodną od nazwy zasobu AWX: przy metadata.name: awx usługa nazywa się awx-service, a pody awx-web-* i awx-task-*. Jeżeli nazwiesz zasób inaczej, wszystkie polecenia niżej trzeba odpowiednio zmienić. Po drugie, pody web i task mają po kilka kontenerów - na maszynie testowej task ma ich cztery, w tym Redis i kontener z obrazem EE. Nie przechodź dalej tylko dlatego, że istnieje Deployment: sprawdź wszystkie kontenery i zdarzenia.
Ingress, TLS i pułapka otwartego HTTP
Ścieżkę Ingress sprawdziliśmy osobno, bez zmieniania działającej instancji: obok istniejącej usługi AWX powstał tymczasowy obiekt Ingress klasy traefik z sekretem TLS zawierającym certyfikat wystawiony lokalnie. Wyniki są jednoznaczne i warto je znać, zanim wystawisz usługę zespołowi.
| Zapytanie | Wynik | Co z tego wynika |
|---|---|---|
HTTPS, poprawny Host | HTTP 200, treść z /api/v2/ping/, certyfikat z sekretu | klasa Ingress, sekret TLS i routing do usługi są poprawnie połączone |
HTTPS, nieznany Host | HTTP 404 z Traefika | 404 przy pierwszym wejściu to zwykle zły hostname lub zła klasa, a nie awaria AWX |
| HTTP na porcie 80 | HTTP 200, pełna odpowiedź aplikacji | samo dodanie sekcji tls nie zamyka zwykłego HTTP |
Trzeci wiersz jest tym, co najczęściej umyka. Traefik w k3s nie ma domyślnie skonfigurowanego przekierowania z portu 80 na 443 - w argumentach jego wdrożenia widać jedynie definicje punktów wejścia i włączony TLS na websecure, bez żadnej reguły redirections. Ingress z sekcją tls obsługuje HTTPS, ale nadal serwuje tę samą aplikację po nieszyfrowanym HTTP. Panel logowania do AWX wystawiony w ten sposób przyjmie hasło przez otwarte połączenie.
Naprawa jest krótka i sprawdzona. Traefik udostępnia zasób Middleware; wystarczy jeden obiekt i jedna adnotacja na Ingressie:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: awx-https-redirect
namespace: awx
spec:
redirectScheme:
scheme: https
permanent: true
Nazwa middleware w adnotacji ma postać <namespace>-<nazwa>@kubernetescrd - jest to częsty punkt pomyłki, bo sam name nie zadziała:
traefik.ingress.kubernetes.io/router.middlewares: awx-awx-https-redirect@kubernetescrd
Po dodaniu tej adnotacji zapytanie na port 80 zwróciło 301 z poprawnym adresem docelowym https://, a HTTPS nadal odpowiadało 200. Klient podążający za przekierowaniem wykonuje dokładnie jeden przeskok.
Gdy Ingressem zarządza operator AWX, adnotację przekazuje się przez pole ingress_annotations. Uwaga na jego typ: w CRD jest to ciąg znaków, a nie mapa, więc trzeba użyć bloku YAML, a nie zagnieżdżonego obiektu:
ingress_annotations: |
traefik.ingress.kubernetes.io/router.middlewares: awx-awx-https-redirect@kubernetescrd
Alternatywą jest globalne przekierowanie na punkcie wejścia Traefika przez HelmChartConfig w namespace kube-system. Rozwiązanie per Ingress jest jednak węższe i nie zmienia zachowania innych usług w klastrze, co przy jednej maszynie z kilkoma aplikacjami zwykle jest bezpieczniejszym wyborem.
Pierwsze wejście do AWX
Jeżeli DNS i certyfikat już działają, otwórz https://awx.example.org/. Pierwsze hasło odczytaj wyłącznie na zaufanym terminalu:
kubectl -n awx get secret awx-admin-password \
-o jsonpath='{.data.password}' | base64 -d
echo
Nie wklejaj wyniku do zgłoszenia, komunikatora ani historii terminala. Zaloguj się jako admin i od razu utwórz zwykłe konta oraz zespoły, zamiast używać administratora do codziennej pracy.
Gdy AWX działa w maszynie wirtualnej, a DNS jeszcze nie ma
Wariant z nazwą DNS zakłada, że rekord już istnieje i wskazuje na maszynę. Zanim to nastąpi - albo jeżeli AWX ma zostać w lokalnym środowisku testowym na stałe - najprostszą drogą jest usługa typu NodePort. Wtedy AWX odpowiada wprost na porcie maszyny, bez Ingressu i bez certyfikatu.
Potrzebne są dwie rzeczy: adres maszyny i numer portu. Adres najpewniej odczytasz z gospodarza wirtualizacji, pytając system w środku przez agenta:
virsh domifaddr awx --source agent
Dopisek --source agent ma znaczenie. Bez niego odczyt idzie z tablicy dzierżaw serwera DHCP (od angielskiego Dynamic Host Configuration Protocol), która bywa nieaktualna - pokazuje adres przydzielony kiedyś, niekoniecznie ten, którego maszyna używa teraz. Odczyt przez agenta pyta system operacyjny gościa o jego rzeczywistą konfigurację. Warunkiem jest zainstalowany w maszynie qemu-guest-agent; jeżeli go nie ma, polecenie nic nie zwróci i trzeba wrócić do wersji bez tego dopisku.
W odpowiedzi zobaczysz kilka adresów - pętlę zwrotną 127.0.0.1 oraz adresy wewnętrzne klastra z zakresu 10.42.0.0/16. Ten właściwy to adres z sieci gospodarza, w typowej instalacji libvirt z zakresu 192.168.122.0/24.
Numer portu odczytasz z samej usługi:
kubectl -n awx get svc awx-service \
-o custom-columns=TYP:.spec.type,PORT:.spec.ports[0].nodePort --no-headers
Mając jedno i drugie, otwierasz w przeglądarce http://ADRES:PORT/. Kubernetes przydziela numery portów tego typu z zakresu 30000-32767, więc będzie to liczba pięciocyfrowa, a nie 80.
Hasło administratora odczytujesz dokładnie tak samo jak wyżej - z sekretu, poleceniem z poprzedniego akapitu. Użytkownik to admin, chyba że zmieniłeś admin_user w manifeście.
Dlaczego port nie widać na liście nasłuchujących
Warto znać ten drobiazg, bo potrafi wysłać na godzinę w złą stronę. Jeżeli sprawdzisz na maszynie, co nasłuchuje:
sudo ss -lntp | grep 30080
prawdopodobnie nie zobaczysz nic - i to nie jest usterka. Kubernetes nie otwiera dla takiego portu zwykłego gniazda nasłuchującego, tylko przekierowuje ruch regułami w warstwie filtrowania pakietów. Reguły znajdziesz przez sudo nft list ruleset | grep 30080. Port działa mimo braku wpisu w ss, więc rozstrzygające jest zwykłe zapytanie:
curl -sS -o /dev/null -w '%{http_code}\n' http://ADRES:PORT/api/v2/ping/
Odpowiedź 200 oznacza, że AWX odpowiada.
Dostęp z innej maszyny niż gospodarz
Domyślna sieć libvirt pracuje w trybie translacji adresów. Maszyna widzi świat, ale świat jej nie - z innego komputera ten adres jest po prostu nieosiągalny. Zamiast otwierać przekierowania na zaporze, przerzuć port tunelem:
ssh -L 8080:ADRES_MASZYNY:PORT uzytkownik@gospodarz
i otwórz http://127.0.0.1:8080/ u siebie. Tunel jest tu lepszy od otwierania portu na zewnątrz, bo dostęp dziedziczy kontrolę z SSH, a nie tworzy drugiej, niezależnej drogi do panelu.
To jest połączenie nieszyfrowane. Przy takim dostępie hasło do panelu idzie przez zwykłe HTTP. W sieci prywatnej, do której nikt inny nie ma dostępu, jest to akceptowalne na czas uruchamiania. W momencie, w którym z AWX ma korzystać ktoś jeszcze, wracasz do wariantu z nazwą DNS i certyfikatem - razem z zamknięciem portu 80, o którym mowa w sekcji o Ingressie, bo samo dodanie certyfikatu go nie zamyka.
Test bez DNS i bez wystawiania portu
port-forward jest bezpieczniejszy od otwierania NodePortu na czas pierwszego testu. Uruchom go w swoim terminalu:
kubectl -n awx port-forward service/awx-service 8080:80
Następnie otwórz http://127.0.0.1:8080/. Tunel działa tylko tak długo, jak proces kubectl, i nie jest zamiennikiem TLS ani reverse proxy. Zwróć uwagę, że usługa AWX nasłuchuje na porcie 80 niezależnie od tego, czy jej typ to ClusterIP, czy NodePort - lokalny port 8080 jest tu tylko wyborem po stronie Twojej maszyny.
Do szybkiej kontroli API bez logowania służy endpoint ping:
curl -fsS https://awx.example.org/api/v2/ping/
Na maszynie testowej odpowiedź zwróciła HTTP 200, wersję 24.6.1, nazwę aktywnego węzła oraz listę grup instancji. To potwierdza, że działa warstwa web i API. Nie zastępuje to testu zadania - web, task i execution environment to osobne elementy i awaria każdego z nich wygląda inaczej.
W tej samej odpowiedzi widać rzecz istotną dla jednowęzłowej instalacji. Grupa instancji controlplane miała jedną instancję i pojemność 79, a grupa default - zero instancji i pojemność 0. Ta zerowa pojemność wygląda na usterkę, a nią nie jest. default jest grupą kontenerową: nie ma stałych instancji, tylko na każde zadanie tworzy w klastrze osobny pod, więc nie ma czego raportować jako pojemność stałą. To właśnie do tej grupy trafiły nasze zadania testowe.
Liczba 79 przy controlplane to z kolei wyliczana przez AWX liczba równoległych procesów Ansible, na jaką pozwalają procesor i pamięć instancji. Nie są to ani zadania, ani hosty: na naszej maszynie 4 rdzenie dały 16, a 7,7 GiB pamięci dało 79, po czym AWX przyjął wyższą z tych dwóch liczb.
Praktyczny wniosek jest inny, niż podpowiada intuicja. Zadania nie wykonują się wewnątrz poda task, tylko w osobnych podach obok niego - ale na jednej maszynie i tak konkurują z samym AWX o ten sam procesor i tę samą pamięć. Granicą nie jest więc liczba instancji, lecz zasoby jedynej maszyny.
Execution environment: skąd bierze się latest
Świeża instalacja AWX 24.6.1 rejestruje trzy globalne execution environmenty. Na maszynie testowej wyglądały tak:
| Nazwa w AWX | Obraz | Rozmiar w cache węzła |
|---|---|---|
| AWX EE (24.6.1) | quay.io/ansible/awx-ee:24.6.1 | 469 MB |
| AWX EE (latest) | quay.io/ansible/awx-ee:latest | 696 MB |
| Control Plane Execution Environment | quay.io/ansible/awx-ee:24.6.1 | ten sam obraz co pierwszy |
Globalne ustawienie DEFAULT_EXECUTION_ENVIRONMENT było przy tym puste. Efekt zobaczyliśmy w praktyce: zadanie uruchomione bez jawnego wskazania środowiska poszło na AWX EE (latest) i pociągnęło za sobą pobranie obrazu latest. Stąd bierze się typowa niespodzianka „przecież podałem konkretną wersję AWX, a i tak ściągnął coś innego”.
Konsekwencje są dwie i obie są mierzalne. Pierwsza to czas: pierwsze zadanie na maszynie testowej trwało 85 sekund, z czego zdecydowana większość przypadła na pobranie obrazu. Druga to powtarzalność - awx-ee:latest i awx-ee:24.6.1 to dwa różne obrazy, różniące się w cache węzła o ponad 200 MB. Zadanie, które dziś działa na latest, jutro może dostać inne wersje kolekcji.
Dlatego w każdym job template deklarujemy execution environment jawnie: albo do konkretnego tagu, na przykład quay.io/ansible/awx-ee:24.6.1, albo do własnego obrazu z wymaganymi kolekcjami. Kiedy projekt potrzebuje kolekcji spoza standardowego EE, buduje się własny obraz, na przykład przez ansible-builder, i wskazuje w szablonie jego tag albo sumę kontrolną obrazu (tak zwany digest), która - w odróżnieniu od tagu - wskazuje zawsze dokładnie tę samą zawartość. Dzięki temu zadanie uruchomione z interfejsu, z API i z harmonogramu ma identyczne zależności. Nie instaluj bibliotek „na próbę” do poda awx-task - znikną przy jego odtworzeniu, a odtworzenie nastąpi przy najbliższej zmianie zasobu AWX.
Pierwszy projekt i pierwszy job
Poniższy przykład celowo wykonuje tylko ansible.builtin.ping. Nie zmienia hosta. Zapisz go w małym, prywatnym repozytorium Git jako ping.yml:
---
- name: Kontrola łączności przez AWX
hosts: linux
gather_facts: false
tasks:
- name: Ansible odpowiada
ansible.builtin.ping:
W interfejsie AWX wykonaj kolejno:
- Organizations → Add - utwórz organizację, na przykład
LinuxLab. - Credentials → Add - wybierz typ Machine; podaj użytkownika SSH i prywatny klucz przeznaczony wyłącznie dla AWX. To poświadczenie do hostów, nie do repozytorium.
- Inventories → Add - utwórz inventory i grupę
linux, a w niej jeden nieprodukcyjny host testowy. Zamiast wpisywać adres IP do kodu projektu, użyj polaansible_hosthosta w inventory. - Projects → Add - wybierz typ SCM - tak AWX nazywa system kontroli wersji, z angielskiego Source Control Management - wskaż Git, wpisz adres repozytorium i uruchom synchronizację. Jeśli repozytorium jest prywatne, dodaj osobne poświadczenie typu Source Control: klucz deploy albo token o minimalnych uprawnieniach.
- Templates → Add → Job Template - wybierz projekt, playbook
ping.yml, inventory, poświadczenie Machine oraz - jawnie - execution environment. - Kliknij Launch i przeczytaj wynik w Jobs, a nie tylko zielony znacznik.
Sukces ping.yml oznacza jednocześnie, że:
- projekt dał się pobrać i zawiera wskazany playbook;
- AWX odczytał właściwe inventory i poświadczenie;
- pod zadania wystartował w execution environment;
- z tego środowiska istnieje połączenie SSH do hosta;
- Ansible wykonał moduł na docelowym hoście.
To dużo silniejszy dowód niż samo otwarcie strony logowania.
Co dokładnie testował job z maszyny testowej
Test celowo pomijał wszystko, co mogłoby zafałszować wynik. Utworzyliśmy tymczasową listę hostów z jednym wpisem - samą maszyną, na której działa AWX - i ustawiliśmy w niej ansible_connection: local. To ustawienie mówi Ansible'owi: nie loguj się nigdzie przez sieć, wykonaj moduł tutaj, w procesie, który już działa. Poświadczenie typu Machine utworzyliśmy puste, bez klucza i bez hasła, bo przy takim połączeniu nie ma się do czego logować. Zamiast całego playbooka uruchomiliśmy pojedyncze polecenie z modułem ping - AWX nazywa to zadaniem ad-hoc, czyli uruchomionym doraźnie z ręki, bez zapisanego szablonu. AWX utworzył dla niego osobny pod o nazwie automation-job-111- z losowym sufiksem - numer w nazwie to numer zadania - a zadanie zakończyło się stanem successful po 85,0 sekundy. Powtórzyliśmy potem ten sam test dwoma zadaniami naraz, już z obrazem w pamięci podręcznej węzła: powstały dwa niezależne pody, a każde zadanie trwało poniżej czterech sekund. To pokazuje, ile z pierwszych 85 sekund poszło wyłącznie na pobranie obrazu. Tymczasowa organizacja, inventory, host i poświadczenie zostały po teście usunięte.
Samo zakończone zadanie pozostaje w historii AWX jako ślad audytowy. Historię zadań traktujemy jako dane operacyjne, a nie jako katalog tymczasowy do mechanicznego czyszczenia - to ona odpowiada później na pytanie, kto i kiedy uruchomił daną zmianę.
Ten test potwierdza więc wyłącznie wewnętrzny łańcuch AWX: że zadanie trafiło do kolejki, doczekało się wolnego miejsca, dostało własny kontener i że kod Ansible w tym kontenerze się wykonał. Nie potwierdza Twojego klucza SSH, dostępu do repozytorium, nazw DNS ani uprawnień na docelowych serwerach. Do tego służy opisany wyżej ping.yml uruchomiony na dedykowanym hoście testowym.
API i tokeny
Interfejs jest wygodny przy pierwszym wdrożeniu, ale do automatyzacji administracji samym AWX lepiej nadaje się API. Utwórz osobisty token w profilu użytkownika, z najmniejszym potrzebnym zakresem, przechowaj go w menedżerze sekretów i używaj bez zapisywania wartości w historii powłoki:
read -r -s -p 'AWX token: ' AWX_TOKEN
echo
curl -fsS \
-H "Authorization: Bearer $AWX_TOKEN" \
https://awx.example.org/api/v2/ping/
unset AWX_TOKEN
Model obiektów AWX bez mieszania pojęć
Większość problemów zgłaszanych po wdrożeniu AWX nie jest awarią, tylko pomyleniem dwóch obiektów o podobnej roli. Poniższa tabela zbiera te, które mylą się najczęściej.
| Obiekt AWX | Do czego służy | Częsty błąd |
|---|---|---|
| Organization / Team | właściciel zasobów i kontrola dostępu | każdy pracuje jako admin |
| Credential: Machine | użytkownik, klucz SSH, ewentualnie podniesienie uprawnień | użycie tego samego klucza na wszystkich środowiskach |
| Credential: Source Control | dostęp do prywatnego repozytorium | wklejenie tokenu Git do poświadczenia Machine |
| Project | kod playbooków w Git i jego synchronizacja | oczekiwanie, że AWX widzi lokalny katalog z laptopa |
| Inventory | hosty, grupy i zmienne | dodanie produkcji do pierwszego testu |
| Execution environment | ansible-core, kolekcje i zależności zadania | założenie, że AWX używa Ansible z maszyny kontrolera |
| Job Template | powtarzalne połączenie projektu, inventory, poświadczeń i EE | ręczne odpalanie zadań z przypadkowymi parametrami |
Diagnostyka: od objawu do dowodu
| Objaw | Sprawdzenie | Najczęstsza przyczyna |
|---|---|---|
Operator nie startuje, ImagePullBackOff | kubectl -n awx describe pod -l control-plane=controller-manager | wycofany obraz z gcr.io; sprawdź podmianę w Kustomize |
| Zasób AWX istnieje, ale brak podów web i task | kubectl -n awx get events --sort-by=.lastTimestamp i log operatora | niegotowe PVC, błąd obrazu albo błąd w manifeście |
| PostgreSQL czeka na PVC | kubectl -n awx get pvc | brak albo inna StorageClass, ewentualnie problem z provisioningiem local-path |
| Traefik zwraca 404 | kubectl -n awx get ingress, kubectl get ingressclass, DNS i nagłówek Host | zła klasa Ingress, inny hostname albo DNS prowadzący gdzie indziej |
| Strona działa po HTTP mimo skonfigurowanego TLS | zapytanie na port 80 i sprawdzenie adnotacji Ingressu | brak przekierowania - Traefik w k3s nie robi go domyślnie |
Zadanie długo stoi w ContainerCreating | kubectl -n awx get pods, kubectl -n awx describe pod automation-job-... | pierwsze pobranie obrazu EE albo problem z rejestrem lub siecią |
| Zadanie nie łączy się przez SSH | wyjście konkretnego zadania, potem ansible_host, użytkownik, klucz i sieć | test lokalny przeszedł, ale poświadczenie lub inventory dla hosta są błędne |
| Po odtworzeniu poświadczenia są nieczytelne | porównaj użyty SECRET_KEY z backupem | odtworzono bazę bez sekretów Kubernetes |
Polecenia zbierające stan bez zmiany klastra:
kubectl -n awx get awx,pods,svc,ingress,pvc
kubectl -n awx get events --sort-by=.lastTimestamp | tail -100
kubectl -n awx logs deployment/awx-operator-controller-manager \
-c awx-manager --tail=200
kubectl -n awx describe awx awx
Backup, restore i upgrade
Najkrótsza zasada brzmi: backup bazy PostgreSQL nie wystarcza. Backup musi obejmować także sekrety współpracujące z bazą, przede wszystkim SECRET_KEY. Operator ma do tego natywne zasoby AWXBackup i AWXRestore; korzystaj z instrukcji dokładnie dla tej wersji operatora, którą masz w klastrze, bo pola tych zasobów zmieniają się między wydaniami: instrukcja backupu i instrukcja odtwarzania dla 2.19.1 oraz oficjalne ostrzeżenia dotyczące aktualizacji.
Praktyczny cykl wygląda tak:
- Utwórz backup i wyeksportuj go poza maszynę.
- Sprawdź sumę kontrolną archiwum.
- Odtwórz backup na oddzielnym, niepublicznym środowisku testowym.
- Zaloguj się, sprawdź odczyt poświadczeń, synchronizację projektu i jedno kontrolowane zadanie.
- Dopiero potem zmieniaj operator lub AWX na produkcji.
Co wolno przy aktualizacji, a czego nie
Sama aktualizacja AWX ma dwa ograniczenia, które projekt stawia wprost i o których lepiej wiedzieć przed wyborem sposobu instalacji, a nie po nim.
Po pierwsze, aktualizacja jest przewidziana wyłącznie dla instalacji przez awx-operator na Kubernetesie - czyli dokładnie dla tego wariantu, który opisuje ten artykuł. Instalacje zrobione inaczej nie mają wspieranej ścieżki podniesienia wersji; zostaje odtworzenie środowiska od nowa i wgranie backupu. To jeden z mocniejszych argumentów za operatorem, nawet na jednej maszynie.
Po drugie, bezpośrednie aktualizacje „w miejscu” między wcześniejszymi wersjami AWX nie są wspierane. Nie da się więc traktować AWX jak zwykłego pakietu i przeskakiwać z dowolnej wersji na dowolną inną. Praktycznie oznacza to, że przy zaległości kilku wydań planujesz nie jedno podbicie, lecz ciąg kroków przez wersje pośrednie, a każdy z nich zaczyna się od backupu.
Przy upgradzie pamiętaj o domyślnym auto_upgrade: true opisanym wyżej. Podbicie operatora bez świadomej decyzji o wersji aplikacji potrafi zmienić obie warstwy naraz, a to jest dokładnie ten moment, w którym najbardziej przydaje się sprawdzony backup.
Jeżeli przenosisz AWX na inną maszynę, nie uruchamiaj jednocześnie harmonogramów, webhooków ani automatycznych synchronizacji inventory na źródle i na odtworzonej kopii. Najpierw zatrzymaj źródło, poczekaj na zakończenie zadań, wykonaj finalny backup, przełącz DNS lub reverse proxy, zweryfikuj cel i dopiero wtedy włącz harmonogramy na celu. Odtworzona kopia ma własną historię i bez tej kolejności potrafi wykonać tę samą automatyzację drugi raz.
Jeszcze jedno pole warto znać przed odtwarzaniem. projects_persistence ma w CRD wartość domyślną false, co oznacza, że katalog projektów nie jest trwały, a projekty typu SCM pobiorą swój kod ponownie po odtworzeniu. To zachowanie normalne i pożądane. Projekty ręczne oraz lokalne pliki wymagają natomiast osobnej procedury kopiowania - nie odtworzą się same.
Ograniczenia tego podejścia
- Jeden węzeł to jeden punkt awarii.
local-pathwiąże dane z dyskiem tej maszyny. Kubernetes nie przeniesie ich sam na inny węzeł, a jednowęzłowy klaster nie daje żadnej redundancji warstwy sterującej. - Wersje operatora i AWX to dwa różne cykle wydawnicze. Kombinacja z tego artykułu została sprawdzona na jednej wersji k3s. Inna wersja Kubernetesa, inne wydanie operatora albo inny obraz bazy to nowy test, a nie kontynuacja tego samego.
- Podmiana obrazu
kube-rbac-proxyjest obejściem, nie stanem docelowym. Trzymamy ją jawnie w Kustomize właśnie po to, żeby przy kolejnym wydaniu operatora sprawdzić, czy nadal jest potrzebna. - Wynik
/api/v2/ping/nie jest testem automatyzacji. Potwierdza web i API. Kolejkę, execution environment i łączność z hostami potwierdza wyłącznie wykonane zadanie. - Jedna maszyna to jedna pula zasobów. Zadania dostają wprawdzie własne pody, ale procesor i pamięć dzielą z samym AWX. Przy większym obciążeniu pierwszym krokiem jest dołożenie węzła, a nie strojenie ustawień AWX.
- Hardening samego klastra jest osobnym tematem. Ten materiał kończy się na wystawieniu AWX po HTTPS. Polityki sieciowe, ograniczenia podów, dostęp do serwera API i rotacja poświadczeń wymagają własnego procesu.
- Historia zadań to dane operacyjne. Zawiera informacje o tym, co i gdzie było uruchamiane. Zasady jej retencji i dostępu ustala się razem z resztą polityki bezpieczeństwa, a nie przy okazji sprzątania bazy.
Checklista przed udostępnieniem zespołowi
- Wygenerowany manifest operatora zawiera oba oczekiwane obrazy, w tym działającą wersję
kube-rbac-proxy. - Manifest AWX przeszedł
kubectl apply --dry-run=serverwobec CRD zainstalowanego w klastrze. kubectl -n awx get awx,pods,svc,ingress,pvcnie pokazuje problemów, a PVC bazy jest w stanieBound.- AWX jest za HTTPS, a nie za publicznym NodePortem po HTTP.
- Zapytanie na port 80 zwraca przekierowanie, a nie stronę aplikacji.
- DNS wskazuje właściwy endpoint, a certyfikat zawiera tę nazwę.
- Hasło administratora i
SECRET_KEYsą w menedżerze sekretów i w backupie, ale nie w repozytorium. - Pierwsze zadanie wykonało się na dedykowanym hoście testowym, a nie na produkcji.
- Poświadczenia Machine są rozdzielone przynajmniej między test i produkcję.
- Job templates używają jawnie zadeklarowanego execution environmentu, nie niekontrolowanego
latest. auto_upgradema wartość ustawioną świadomie, a nie domyślną przez przeoczenie.- Backup i odtworzenie zostały przećwiczone poza produkcją.
- Instalacja jest oparta na awx-operator, czyli ma wspieraną ścieżkę aktualizacji, a plan podniesienia wersji uwzględnia wersje pośrednie.
- Harmonogramy, webhooki i synchronizacje inventory mają po migracji jednego aktywnego właściciela.
- Konta zwykłych użytkowników istnieją, a
adminnie jest kontem roboczym.
AWX zwraca zainwestowany czas dopiero wtedy, gdy przestaje być kolejnym miejscem do ręcznego klikania. Wartość bierze się z zadeklarowanych wersji, jawnie wskazanego execution environmentu, rozdzielonych poświadczeń i sprawdzonego odtworzenia - a nie z tego, że panel się otworzył. Dlatego każdy krok w tym materiale ma przypisany sposób weryfikacji, a to, czego nie udało się zmierzyć, jest w nim nazwane wprost.
Potrzebujesz AWX, który udźwignie pracę zespołu?
Projektujemy instalację, deklarujemy wersje, zamykamy dostęp za HTTPS i ustawiamy backup razem z odtworzeniem - a potem sprawdzamy to jednym kontrolowanym zadaniem.
Powiązany artykuł
Ansible od podstaw: playbooki, role, idempotencja i Vault
Kod, który AWX uruchamia - zanim trafi do projektu i job template.
Zobacz też