Przejdź do treści
Automatyzacja i DevOps·~35 min czytania

AWX na k3s: instalacja, pierwszy job i bezpieczna eksploatacja

Powtarzalna instalacja pojedynczej instancji AWX na jednej VM: zadeklarowane wersje, wycofany obraz w manifeście operatora, Ingress z TLS oraz pierwszy job, który naprawdę coś dowodzi.

Autor: Zespół LinuxLab

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

  1. Co sprawdziliśmy, a czego nie
  2. Krótki słownik
  3. Jak wygląda przepływ
  4. Decyzje przed instalacją
  5. Maszyna wirtualna krok po kroku
  6. Instalacja k3s
  7. Wersje i martwy obraz
  8. Sekrety, SECRET_KEY i TLS
  9. Manifest AWX
  10. Ingress, TLS i otwarte HTTP
  11. Pierwsze wejście do AWX
  12. Skąd bierze się latest
  13. Pierwszy projekt i pierwszy job
  14. Model obiektów bez mieszania pojęć
  15. Diagnostyka: od objawu do dowodu
  16. Backup, restore i upgrade
  17. Ograniczenia tego podejścia
  18. 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.

ElementStan zmierzony 10 września 2026
MaszynaUbuntu 24.04.4 LTS, jądro 6.8, 4 vCPU, 7,7 GiB RAM, dysk 38 GiB użytkowego, 26 GiB wolnego
Kubernetesjednowęzłowy k3s v1.34.3+k3s3, node w stanie Ready, runtime containerd 2.1.5
Storage i ingressdomyślna StorageClass local-path, IngressClass o nazwie traefik
AWXAWX Operator 2.19.1, AWX 24.6.1, baza z obrazu quay.io/sclorg/postgresql-15-c9s
Podyoperator, 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 proxyw podzie operatora działa quay.io/brancz/kube-rbac-proxy:v0.15.0, nie obraz z gcr.io
Manifest operatorapolecenie kubectl kustomize (Kustomize 5.7.1 wbudowany w kubectl 1.34.3) wygenerowało manifest zawierający oba oczekiwane obrazy
Manifest AWXpokazany 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 TLSIngress klasy traefik z sekretem TLS: HTTPS zwróciło 200 i odpowiedź AWX; nieznany Host zwrócił 404 z Traefika
Wykonanie automatyzacjipojedyncze 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-run zasobu 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 web i task oraz 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 AWX tworzy 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, AWXBackup i AWXRestore.
  • 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 AWX w ogóle istnieje, a CR to już konkretny AWX o konkretnej nazwie. Plik awx.yaml z tego artykułu jest właśnie CR-em typu AWX.
  • 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 awx w 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 nazwie automation-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ę.

  1. 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.
  2. 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.
  3. 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ą.
  4. 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:

  1. Przygotuj maszynę wirtualną - procesor, pamięć, dysk, system i dostęp SSH.
  2. Sprawdź jej gotowość - czas, uprawnienia i wyjście na zewnątrz. Dowód: cztery polecenia z tej sekcji.
  3. Zainstaluj k3s. Dowód: jeden węzeł w stanie Ready.
  4. Wygeneruj manifest operatora i sprawdź obrazy. Dowód: dwie linie z grep.
  5. Utwórz sekrety - hasło administratora i SECRET_KEY.
  6. Wdróż operator, potem zasób AWX. Dowód: pody web, task i baza w stanie Running.
  7. Wystaw usługę przez HTTPS. Dowód: HTTP 200 z /api/v2/ping/ i przekierowanie z portu 80.
  8. Uruchom pierwsze zadanie. Dowód: stan successful na 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óbMinimum k3s wg dokumentacjiUżyte dla AWXDlaczego tyle
Procesor2 rdzenie (węzeł serwera)4 rdzenieAWX 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 GiBSam AWX z bazą i Redisem zajmuje kilka GiB w spoczynku. Na maszynie testowej przy bezczynnym AWX zajęte było 3,3 GiB.
Dyskzalecany SSD40 GiBSame 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 LTSWersja, 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_type przyjmuje wyłącznie LoadBalancer, ClusterIP lub NodePort (także pisane małymi literami). Inna wartość zostanie odrzucona przez API, a nie po cichu zignorowana.
  • ingress_type przyjmuje none, Ingress, ingress, Route lub route. Wariant Route dotyczy OpenShiftu, nie k3s.
  • ingress_hosts jest listą obiektów z polami hostname i tls_secret. Starsze pola hostname i ingress_tls_secret są w tym CRD oznaczone jako przestarzałe - nie mieszaj obu zapisów.
  • auto_upgrade ma 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, ustaw false jawnie 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.

ZapytanieWynikCo z tego wynika
HTTPS, poprawny HostHTTP 200, treść z /api/v2/ping/, certyfikat z sekretuklasa Ingress, sekret TLS i routing do usługi są poprawnie połączone
HTTPS, nieznany HostHTTP 404 z Traefika404 przy pierwszym wejściu to zwykle zły hostname lub zła klasa, a nie awaria AWX
HTTP na porcie 80HTTP 200, pełna odpowiedź aplikacjisamo 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 AWXObrazRozmiar w cache węzła
AWX EE (24.6.1)quay.io/ansible/awx-ee:24.6.1469 MB
AWX EE (latest)quay.io/ansible/awx-ee:latest696 MB
Control Plane Execution Environmentquay.io/ansible/awx-ee:24.6.1ten 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:

  1. Organizations → Add - utwórz organizację, na przykład LinuxLab.
  2. 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.
  3. Inventories → Add - utwórz inventory i grupę linux, a w niej jeden nieprodukcyjny host testowy. Zamiast wpisywać adres IP do kodu projektu, użyj pola ansible_host hosta w inventory.
  4. 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.
  5. Templates → Add → Job Template - wybierz projekt, playbook ping.yml, inventory, poświadczenie Machine oraz - jawnie - execution environment.
  6. 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 AWXDo czego służyCzęsty błąd
Organization / Teamwłaściciel zasobów i kontrola dostępukażdy pracuje jako admin
Credential: Machineużytkownik, klucz SSH, ewentualnie podniesienie uprawnieńużycie tego samego klucza na wszystkich środowiskach
Credential: Source Controldostęp do prywatnego repozytoriumwklejenie tokenu Git do poświadczenia Machine
Projectkod playbooków w Git i jego synchronizacjaoczekiwanie, że AWX widzi lokalny katalog z laptopa
Inventoryhosty, grupy i zmiennedodanie produkcji do pierwszego testu
Execution environmentansible-core, kolekcje i zależności zadaniazałożenie, że AWX używa Ansible z maszyny kontrolera
Job Templatepowtarzalne połączenie projektu, inventory, poświadczeń i EEręczne odpalanie zadań z przypadkowymi parametrami

Diagnostyka: od objawu do dowodu

ObjawSprawdzenieNajczęstsza przyczyna
Operator nie startuje, ImagePullBackOffkubectl -n awx describe pod -l control-plane=controller-managerwycofany obraz z gcr.io; sprawdź podmianę w Kustomize
Zasób AWX istnieje, ale brak podów web i taskkubectl -n awx get events --sort-by=.lastTimestamp i log operatoraniegotowe PVC, błąd obrazu albo błąd w manifeście
PostgreSQL czeka na PVCkubectl -n awx get pvcbrak albo inna StorageClass, ewentualnie problem z provisioningiem local-path
Traefik zwraca 404kubectl -n awx get ingress, kubectl get ingressclass, DNS i nagłówek Hostzła klasa Ingress, inny hostname albo DNS prowadzący gdzie indziej
Strona działa po HTTP mimo skonfigurowanego TLSzapytanie na port 80 i sprawdzenie adnotacji Ingressubrak przekierowania - Traefik w k3s nie robi go domyślnie
Zadanie długo stoi w ContainerCreatingkubectl -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 SSHwyjś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ą nieczytelneporównaj użyty SECRET_KEY z backupemodtworzono 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:

  1. Utwórz backup i wyeksportuj go poza maszynę.
  2. Sprawdź sumę kontrolną archiwum.
  3. Odtwórz backup na oddzielnym, niepublicznym środowisku testowym.
  4. Zaloguj się, sprawdź odczyt poświadczeń, synchronizację projektu i jedno kontrolowane zadanie.
  5. 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

  1. Jeden węzeł to jeden punkt awarii. local-path wiąż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.
  2. 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.
  3. Podmiana obrazu kube-rbac-proxy jest 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.
  4. Wynik /api/v2/ping/ nie jest testem automatyzacji. Potwierdza web i API. Kolejkę, execution environment i łączność z hostami potwierdza wyłącznie wykonane zadanie.
  5. 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.
  6. 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.
  7. 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

  1. Wygenerowany manifest operatora zawiera oba oczekiwane obrazy, w tym działającą wersję kube-rbac-proxy.
  2. Manifest AWX przeszedł kubectl apply --dry-run=server wobec CRD zainstalowanego w klastrze.
  3. kubectl -n awx get awx,pods,svc,ingress,pvc nie pokazuje problemów, a PVC bazy jest w stanie Bound.
  4. AWX jest za HTTPS, a nie za publicznym NodePortem po HTTP.
  5. Zapytanie na port 80 zwraca przekierowanie, a nie stronę aplikacji.
  6. DNS wskazuje właściwy endpoint, a certyfikat zawiera tę nazwę.
  7. Hasło administratora i SECRET_KEY są w menedżerze sekretów i w backupie, ale nie w repozytorium.
  8. Pierwsze zadanie wykonało się na dedykowanym hoście testowym, a nie na produkcji.
  9. Poświadczenia Machine są rozdzielone przynajmniej między test i produkcję.
  10. Job templates używają jawnie zadeklarowanego execution environmentu, nie niekontrolowanego latest.
  11. auto_upgrade ma wartość ustawioną świadomie, a nie domyślną przez przeoczenie.
  12. Backup i odtworzenie zostały przećwiczone poza produkcją.
  13. Instalacja jest oparta na awx-operator, czyli ma wspieraną ścieżkę aktualizacji, a plan podniesienia wersji uwzględnia wersje pośrednie.
  14. Harmonogramy, webhooki i synchronizacje inventory mają po migracji jednego aktywnego właściciela.
  15. Konta zwykłych użytkowników istnieją, a admin nie 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.