Zależności pomiędzy usługami

Korzystając z Kubernetesa czasami chcemy uzależnić uruchomienie usługi uruchamianej na jednym z podów od zainicjowania się usługi uruchamianej na innym podzie.

Załóżmy, że mamy następujące wymaganie. Każde wdrożenie nowej wersji aplikacji powinno być automatycznie testowane przez usługę testującą uruchamianą na niezależnym podzie, która przeprowadza testy funkcjonalne tej aplikacji. Zarówno aplikację jak usługę testującą wdrażamy na środowisko jednocześnie lecz niezależnymi skryptami, a test uruchamia się od razu po uruchomieniu poda z usługa testującą.

Potrzebujemy  zapewnić aby  kontener z usługą testującą uruchomiony został  po starcie i zainicjowaniu się poda z aplikacją.

Możliwe podejścia

Kubernetes niestety nie udostępnia narzędzi, które pozwoliłyby w prosty sposób spełnić takie wymaganie.

Jeśli wdrażamy aplikację i testy za pomocą oddzielnych chartów Helm możemy wykorzystać flagę –wait. Jeśli ta flaga jest ustawiona komenda helm install/upgrade zakończy swoje działanie dopiero jak wszystkie obiekty Kubernetes zostaną utworzone lub będą miały minimalną ilość podów. Niestety komenda może też się zakończyć błędem jeśli przekroczymy maksymalny czas oczekiwania które określany jest przez flagę –timeout (domyślnie 5min).

Jeśli jednak nie korzystamy z  Helm’a, możemy wykorzystać mechanizm InitContainers.

InitContainers (kontenerty inicjujące) służą zazwyczaj do przygotowania inicjalnego środowiska dla kontenera z aplikacją. Możemy je jednak wykorzystać do tego aby opóźnić uruchomienie kontenera głównego do czasu aż nasze środowisko będzie w odpowiednim stanie.

Pojedynczy Pod może mieć wiele InitContainers które uruchamiają się sekwencyjnie i muszą się zakończyć poprawnie (Exit code: 0) aby kontener docelowy z aplikacją mógł zostać uruchomiony. Uwaga pole restart Policy umożliwia sterowanie procesem inicjowania w przypadku gdy pod zwróci kod 1. Jeżeli nie ustawimy restartPolicy: Never zwrócenie kodu 1 przez kontener inicjujący będzie powodowało jego ponowny restart aż do momentu poprawnego uruchomienia (Exit code:0).

Możemy także konfigurować wiele kontenerów inicjujących do poda. Wszystkie one muszą uruchomić się poprawnie. Będą one uruchamiane jeden po drugim. I każdy musi zakończyć się poprawnie aby uruchomiony został kolejny.

Przykład rozwiązania z użyciem InitContainers

Wykorzystajmy więc ten mechanizm do skonfigurowania kontenera inicjującego dla naszego kontenera głównego z usługą testującą. Kontener ten będzie weryfikować czy usługa, którą testujemy jest już aktywna poprzez weryfikacje skryptem zwracającym 0 w przypadku wykrycia poprawnego stanu usługi którą ma testować.

Załóżmy, że aplikacja którą testujemy jest aplikacją Spring Boot która udostępnia endpoint /actutator/health z modułu Spring Boot Actuator. Posłuży on nam do sprawdzenia czy aplikacja została już zainicjowana. Usługa którą testujemy jest udostępniana przez Service app-service. Obraz z usługa testującą to: app-tests.

Do weryfikacji stanu posłuży nam skrypt sprawdzający co minutę czy aplikacja jest poprawnie uruchomiona i w przypadku wyrycia takiego stanu zwracający 0. Jeśli w ciągu 5min (5 prób w odstępie 1min) nie stwierdzimy że aplikacja jest uruchamiana wtedy zwróci exit code 1.

Wtedy definicja Job’a uruchamiającego usługę testującą może wyglądać następująco:

apiVersion: batch/v1 
kind: Job 
metadata: 
    name: test-job 
spec: 
    template:
        spec: 
             restartPolicy: Never 
             containers: 
             - name: app-tests 
             image: app-tests:latest 
             initContainers: 
             - name: wait-for-app 
             image: alpine-curl:latest 
             command: [ 
                 "sh", 
                 "-c", 
                 "NUMBER_OF_TRIES=0; 
                  sleep 2m; 
                  IS_UP=$(curl -f -s --connect-timeout 5 app-service:8080/actuator/health | grep -c '\"status\":\"UP\"'); 
                  while [[ \"$NUMBER_OF_TRIES\" -lt 5 ]] && [[ \"$IS_UP\" != 1 ]]; 
                      do 
                          let \"NUMBER_OF_TRIES+=1\"; 
                          echo \"App not running, wait 1min, number of tries $NUMBER_OF_TRIES...\"; 
                          sleep 1m; 
                          IS_UP=$(curl -f -s --connect-timeout 5 app-service:8080/actuator/health | grep -c '\"status\":\"UP\"'); 
                      done; 
                          if [[ \"$IS_UP\" == 1 ]]; 
                              then echo App is up and running; exit 0; 
                              else echo App is down; exit 1;
                          fi"]

Nasz kontener app-test ma zdefiniowany initConteiner o nazwie wait-for-app, który uruchomi się przed kontenerem app-test i będzie czekał aż skrypt wykryje poprawne uruchomienie się aplikacji . W takim przypadku zakończy pracę poprawnie i kontener app-test zostanie uruchomiony i rozpocznie testy aplikacji gdy będzie ona już zainicjowana i dostępna.

Jeśli w ciągu 5min (5 prób w odstępie 1min) aplikacja nie zostanie zainicjowana skrypt zwróci wartość 1. Będzie to sygnałem dla Kubernetes’a, że nasz kontener inicjujący zakończył się „niepoprawnie”. W naszym przypadku restartPolicy jest ustawiony na wartość Never, nie zostanie więc uruchomiony kontener właściwy na Podzie ani nie będzie kolejnych prób jego zainicjowania.

W zależności od wymagań i konkretnego przypadku posługując się parametrami można dostosować ten mechanizm pod własne potrzeby.