Backend

Dwa tygodnie z Kubernetesem na produkcji

W ostatnich latach model projektowania architektury aplikacji w formie mikroserwisów zaczął wypierać model monolityczny z wielu powodów, o których można by popełnić osobny artykuł. Konteneryzacja pozwala na dzielenie naszej aplikacji na mniejsze, niezależne części (usługi), które można zaaranżować i skomunikować ze sobą na wiele różnych sposobów. Kontener w takiej architekturze jest niczym innym jak programem działającym w odizolowanym środowisku, które posiada tylko zależności potrzebne do działania uruchamianego procesu.

Daje nam to możliwość budowania wysoce skalowalnych i solidnych rozwiązań. To z kolei powoduje, iż tworzenie zaplecza IT staje się jeszcze łatwiejsze z uwagi na brak konieczności utrzymania własnej, dedykowanej infrastruktury IT.


Wojciech Pawlinów. Back-End Developer w firmie Software Brothers. Zawodowo programuje w Pythonie i PHP, prywatnie rozwija się w języku Go w kierunku pracy przy systemach rozproszonych. Od zawsze związany z DevOpsem, interesuje się projektowaniem rozwiązań High Availability. Entuzjasta systemu Linux, aplikacji Cloud Native oraz poznawania nowych technologi i języków programowania. Po godzinach dba o work-life balance aktywnie ćwicząc CrossFit.


Jednakże dla programistów dotychczas pracujących z monolitami, których nowym zadaniem jest utrzymywanie aplikacji składającej się z mikroserwisów, takie rozwiązanie ma także kilka minusów. Teraz ta sama aplikacja posiada kilka, nieraz kilkanaście subprocesów, które trzeba ze sobą odpowiednio skomunikować, debugować i testować jako całość. Oznacza to tyle, że musimy wdrożyć, czy to lokalnie czy produkcyjnie, wszystkie te mikrousługi, aby dostarczyć jeden działający system. W zależności od tego, jak projekt jest przygotowany i udokumentowany, wdrożenie i rozwijanie całej aplikacji może nie być łatwym zadaniem.

Wdrożenie produkcyjne, wersjonowanie oraz utrzymanie działania aplikacji bez przerw jest nie lada wyzwaniem. Potrzeba ludzi doświadczonych, obeznanych z zagadnieniami DevOps i umiejących zaprojektować procesy i każdą warstwę architektury, tak aby dostarczać rozwiązania o wysokiej dostępności, które dodatkowo automatycznie się skalują. To oczywiste, że żaden produkt nie powinien być zależny od członków zespołu projektowego, ze względu na ryzyko zmian wewnątrz zespołu. Praca z oprogramowaniem i serwerami powinna być na tyle łatwa, żeby nowe osoby mogły bez trudu pracować i rozwijać aplikację. Zaplecze IT dla oprogramowania musi być łatwe w obsłudze oraz konfiguracji, aby oszczędzać czas, pieniądze i zmniejszać ryzyko wystąpienia problemów.

Istnieje wiele innych wyzwań związanych z praktykami DevOps, które sprowadzają nas do głównej myśli tego artykułu: dlaczego zdecydowaliśmy się użyć Kubernetesa?

Dlaczego Kubernetes?

W Software Brothers, jak i pewnie w wielu innych software housach, powszechną praktyką jest wspieranie wielu środowisk deweloperskich na potrzeby każdego projektu (zazwyczaj są to: development, staging i production). Nawet jeśli potrzebujemy wszystkich trzech instancji środowisk, nie ma potrzeby, aby posiadały one jednakową konfigurację. Przykładowo: API w wersji przeznaczonej dla programistów rozwijających aplikację nie wymaga tak dużych zasobów procesora, jak wersja produkcyjna, która ma obsługiwać tysiące zapytań na sekundę. Z drugiej strony, serwer, na którym docelowo ma działać dana aplikacja, wymaga większej ilości miejsca na dysku, więcej RAM-u czy innych zasobów, które w wersji “tylko dla klienta” nie są potrzebne.

W celu hostowania aplikacji stosujemy rozwiązania chmurowe zamiast budować i utrzymywać do tych celów własną platformę. Daje nam to mnóstwo możliwości, a także elastyczność i łatwość konfiguracji. Do tej pory wdrażaliśmy aplikacje używając głównie Amazon ECS. Przez ostatnie lata zdobyliśmy w tej kwestii sporo doświadczenia — zatem większość naszych projektów korzysta ze sposobu orkiestracji dostarczanej przez Amazona. Ostatnio potrzebowaliśmy pójść o krok dalej i wdrożyć jeden z naszych ostatnich projektów w środowisku klastrowym. Korzystaliśmy przy tym z Kubernetesa.

Jednym z kluczowych powodów użycia “k8s” jest to, że konfiguracja, którą tworzymy nie jest zależna od providera, stąd jesteśmy w stanie zmienić platformę i, korzystając z tego samego kodu, odtworzyć identyczny stack przy użyciu innej chmury lub nawet dedykowanej infrastruktury serwerowej.

Poza tym Kubernetes jest bardzo popularnym rozwiązaniem — dobrze udokumentowanym, stworzonym i wspieranym przez firmę Google. Istnieje kilka wartościowych alternatyw, takich jak Docker Swarm, Apache Mesos, HashiCorp Nomad czy Kontena, i mają one spore grono użytkowników. Niemniej, Kubernetes jest projektem Open Source, który na obecną chwilę liczy ponad 45 tys. gwiazdek na GitHubie (jest napisany w języku Go). Warto również dodać, że dzięki aplikacji minikube można korzystać z niego lokalnie, bez konieczności wykupu usług u dostawców chmury internetowej.

Kolejnym powodem, dla którego według nas warto było skorzystać z Kubernetesa, jest fakt, że nie trzeba w żaden sposób adaptować kodu aplikacji w celu wdrożenia jej na inną architekturę. Lubimy koncepcję podejścia “architektura jako kod”. Wszystkie zasoby i zachowanie definiuje się w plikach konfiguracyjnych oraz trzyma się je w systemie kontroli wersji, co pozwala na śledzenie historii zmian. Ponadto, formatem konfiguracji jest YAML, co zwiększa czytelność kodu. Jeśli wgrywasz zmiany w klastrze poprzez aktualizację plików konfiguracyjnych, Kubernetes dokonuje autoformatowania i dodaje od siebie sporo informacji szczegółowych. Do obsługi klastra istnieje interfejs linii poleceń kubectl, który służy do komunikacji z API serwerem znajdującym się na Master Node, poprzez wskazanie pliku *.yaml z definicją zasobu lub bezpośrednio w parametrach polecenia.

Mnogość automatyzacji, które Kubernetes oferuje, jest kolejnym powodem do zainteresowania się tą technologią. Potrafi automatycznie zarządzać zmianami i przywracaniem poprzednich konfiguracji po tym, jak wystąpią jakieś błędy. Świetnie radzi sobie z montowaniem wolumenów różnego rodzaju, czy to lokalnych czy sieciowych. Zapewnia automatyczne przydzielanie adresów kontenerom i komunikację między nimi. Potrafi również na podstawie konkretnie sprecyzowanych wymagań dotyczących zasobów zarządzać kontenerami oraz rozmieszczać je na poszczególnych maszynach bez ograniczania dostępności.

Czy istnieje jakiś powód, dla którego nie powinniśmy zatem korzystać z Kubernetesa? Jedną rzeczą, jaka może zastanawiać ludzi bez wcześniejszego doświadczenia z tą technologią, jest odpowiedź na pytanie: jak to zrobić dobrze?

Po naszej dwutygodniowej przygodzie z tą technologią wyciągnęliśmy kilka wniosków na ten temat.

Jak korzystamy z Kubernetesa?

Przede wszystkim, piszemy nasze Dockerfiles korzystając z Builder Pattern, aby nasze kontenery były tak małe i lekkie, jak to tylko możliwe. Trzymamy się również zasady pojedynczej odpowiedzialności, a więc nie łączymy w ramach kontenera wielu procesów i nie mamy zależności. Nie trzymamy danych na kontenerze. Używając obrazów dockerowych w wersji alpine, zmniejszamy rozmiar kontenera, co pozytywnie wpływa na czas budowania oraz wysyłania obrazów na serwer, a także zmniejsza zużycie miejsca na dysku.

Dodatkowo, jeśli budujemy obraz produkcyjny, to nie tagujemy go jako latest, ale oznaczamy go pierwszymi ośmioma znakami z commit hasha (Git).

W Dockerfile ustawiamy system plików tylko do odczytu (ang. read-only) oraz uruchamiamy aplikację jako normalny użytkownik, tak aby uniknąć tworzenia procesów z prawami roota. Dodam, że dzięki obrazom z izolacją przestrzeni nazw poprawiamy bezpieczeństwo aplikacji, poprzez ograniczenie dostępu do jej zasobów.

Kolejną ważną zasadą jest uruchamianie pojedynczego procesu w ramach jednego kontenera. Mimo że jest to dość oczywiste, warto o tym wspomnieć jako o dobrej praktyce. Takie podejście daje skalowalność horyzontalną. Co więcej, cały system jest bardziej elastyczny i jego funkcjonalności są od siebie odizolowane. Dzięki temu łatwiej zbierać logi lub wykorzystać tak skonteneryzowaną usługę w innym projekcie. Dla przykładu, mając dwa procesy w jednym kontenerze istnieje ryzyko, że jeden z nich nagle przestanie działać. Docker agent powinien automatycznie przechwycić takie zdarzenie, usunąć taki kontener oraz zastąpić go nowym. Tymczasem taki kontener nadal istnieje i jest rozpoznawany jako “zdrowy” z uwagi na nieprzerwanie pracujący drugi proces. Mamy wtedy do czynienia z kontenerem “zombie”.

Wszystkie deployments tworzymy w taki sposób, żeby zawsze reprezentowały jeden mikroserwis. Tak samo jak w powyższym paragrafie, jednakże z punktu widzenia Kubernetesa. Każdy deployment, jeśli to możliwe, zawiera jeden kontener na jeden pod. Czasami zdarza się, że wymagany jest jeszcze jeden kontener (na przykład proxy). Należy jednak projektować deployment w taki sposób, aby móc go replikować bez ryzyka wystąpienia problemów w komunikacji z resztą usług. Tylko dzięki takiemu podejściu możemy zapewnić skalowalność.

Korzystamy z flagi –record w celu śledzenia zmian w zasobach klastra. Dla przykładu polecenie kubectl rollout history deployments pokaże nam ostatnie zmiany w naszej konfiguracji deployments. W początkowej fazie prac, gdzie wykonywaliśmy sporo zmian z linii komend, nie korzystaliśmy z tej opcji, co poskutkowało zgubieniem kontekstu pracy.

Jeżeli tworzymy wiele obiektów YAML zależnych od siebie, takich jak na przykład PersistentVolume i PersistentVolumeClaim, to ich kod trzymamy w jednym pliku.

Liczymy zasoby klastra. Warto pamiętać o takich parametrach jak requests i limits, w których ustawiamy wartości jakich oczekujemy w fazie tworzenia deploymentu oraz ich limity w ramach jednego poda. W kontekście strategii RollingUpdate definiuje się też istotne parametry, takie jak maxSurge i maxUnavailable, które określają zachowanie w przypadku replikacji podów (link do dobrego artykułu opisującego przykłady użycia). Bez tych parametrów istnieje ryzyko, że aplikacja będzie miała problem z powołaniem do życia nowych podów, z uwagi na, przykładowo, brak dostępnego CPU. Przed podejściem do konfiguracji klastra, dobrze jest przekalkulować różne przypadki użycia procesora i pamięci, a także innych środków dostępnych w ramach puli zasobów maszyny.

Ostatnia z naszych rad to wykorzystanie Helm, menadżera paczek dla Kubernetesa. Helm pomaga w znalezieniu i instalacji gotowych aplikacji w formie szablonów konfigurowalnych poprzez definiowanie kluczy i wartości w pliku values.yaml. Dzięki temu możemy wydzielić część naszej aplikacji w postaci szablonu i w przyszłości bardzo szybko wdrożyć ją z łatwością lub podzielić się nią ze społecznością. Dzięki zmiennym możemy dostosowywać wszelkie parametry w ramach szablonu.

Ostatecznie zastosowaliśmy jednak trochę inne podejście — krok po kroku budowaliśmy naszą aplikację poprzez pisanie całej konfiguracji bez pomocy Helm charts. Chcieliśmy lepiej poznać Kubernetesa. Kiedy osiągnęliśmy to, na czym nam zależało, dodaliśmy szablony do projektu. Zawsze staramy się poznać rozwiązanie “od podszewki” zanim zacznę cokolwiek robić, zwłaszcza produkcyjnie. I tak było tym razem, podczas wydawania aplikacji dla naszego klienta, skupiliśmy się na dobrych praktykach w każdej części architektury kosztem czasu, który spędziliśmy.

Ostatnie kroki — wdrożenie na produkcję

Jednymi z ostatnich rzeczy związanych z wydaniem aplikacji użytkownikom było przekierowanie DNS i dostarczenie bezpiecznego połączenia poprzez certyfikację SSL. Mieliśmy działającą aplikację, dostępną pod adresem IP, jednak bez szyfrowanej komunikacji. Udało się dodać SSL w bardzo mało elegancki sposób, ale pojawiło się pytanie: co w sytuacji, kiedy certyfikat wygaśnie i aplikacje mobilne przestaną poprawnie komunikować się z naszym API? Należało sprawić, by certyfikat odnawiał się automatycznie. Znaleźliśmy na to sposób, zobaczmy:

Na początku trzeba stworzyć Ingress, który udostępnia zewnętrzny dostęp do usług działających w klastrze. Jak mówi dokumentacja, Ingress to obiekt, który służy m.in. do load balancingu, SSL termination i name-based virtual hostingu.

Każdy service musi jednak posiadać definicję pól readinessProbe i livenessProbe, aby Ingress mógł rozpoznać na tej podstawie, jaki status ma dany service.

(Fragment konfiguracji deploymentu, container spec)
livenessProbe:
    failureThreshold: 3
    httpGet:
      path: /
      port: 8000
      scheme: HTTP
    initialDelaySeconds: 60
    periodSeconds: 65
    successThreshold: 1
    timeoutSeconds: 1

readinessProbe:
    failureThreshold: 3
    httpGet:
      path: /
      port: 8000
      scheme: HTTP
    initialDelaySeconds: 50
    periodSeconds: 55
    successThreshold: 1
    timeoutSeconds: 1

Dla każdego kontenera wymagana jest odpowiedź z kodem 200 na żądanie GET /. Można też zdefiniować inny, niestandardowy health-check.

Obiekt Ingress ma następującą strukturę:

(production-ingress.yaml)

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    certmanager.k8s.io/cluster-issuer: our_cluster_issuer
    kubernetes.io/ingress.global-static-ip-name: our_static_ip
    kubernetes.io/tls-acme: "true"
  name: production-ingress
  namespace: default
spec:
  backend:
    serviceName: our_service
    servicePort: 80
  tls:
  - secretName: our_secret_with_certificates
status:
  loadBalancer:
    ingress:
    - ip: xxx.xxx.xxx.xxx

Należy zwrócić uwagę na pole annotations. Potrzebujemy statycznego adresu IP (w naszym przypadku: stworzonego jako usługę Google Cloud Platform), serwisu (service) wskazującego konkretny deployment, serwujący naszą aplikację, obiektu cluster issuer oraz czegoś, co można znaleźć na powyższym fragmencie kodu w sekcji spec — definicji obiektu secret, który przetrzymuje dwie wartości: cert i key.

Poniżej kod wspomnianych obiektów:

(our_secret_with_certificates.yaml)
apiVersion: v1
data:
  tls.crt: # crt
  tls.key: # key
kind: Secret
metadata:
  name: our_secret_with_certificates
  namespace: default
type: Opaque

(our_cluster_issuer.yaml)

apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
  name: our_cluster_issuer
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: your-registration-address@email.com
    privateKeySecretRef:
      name: our_cluster_issuer
    selfSigned: {}

Ponadto, trzeba stworzyć certificate:

(our_certificate.yaml)
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
  name: our_certificate
spec:
  secretName: our_secret_with_certificates
  dnsNames:
  - our.domain
  acme:
    config:
    - http01:
        ingress: production-ingress
      domains:
      - our.domain
  issuerRef:
    name: our_cluster_issuer
    kind: ClusterIssuer

Tworząc w klastrze powyższe konfiguracje (kubectl create -f [ścieżka]/[nazwa pliku].yaml), udało nam się dostarczyć certyfikat SSL, który automatycznie się odnawia.

Podsumowanie

Moje pierwsze wdrożenie za pomocą Kubernetesa zajęło trochę więcej, niż początkowo szacowałem. Sporo zajęło mi wczytywanie się w dokumentację i tutoriale, ale brakowało mi konkretnych case-study dla moich przypadków. Wielokrotnie poprawiałem swoje konfiguracje, kiedy natknąłem się w sieci na inne, ładniejsze rozwiązania.

Kilka osób pytało mnie jak trudno jest zacząć korzystać z Kubernetesa. Z tej perspektywy mogę powiedzieć, że jest tak samo trudno, jak z każdą inną nową technologią, której nie znamy — im więcej czasu przy tym spędzimy, tym prostsza się wydaje. Trzeba metodą prób i błędów wypracować odpowiednie rozwiązania, ale warto najpierw zapoznać się z tym, jak inni je realizują. W serwisie GitHub można znaleźć bardzo dużo przykładów dołączonych do projektów. Można je lokalnie przetestować za pomocą minikube i próbować dostosować do swoich potrzeb.

Myślę, że Kubernetes bardzo upraszcza cały proces prac wdrożeniowych, a przy tym dostarcza wszystko to, czego potrzebujemy, aby stworzyć nawet najbardziej wymagającą architekturę aplikacji. Osobiście chciałbym mieć okazję poznać w praktyce resztę jego możliwości.

Jeżeli chcesz wykorzystać go w swoim projekcie, przemyśl najpierw, czy faktycznie potrzebujesz, aby Twoja aplikacja działała w środowisku klastrowym. Jeśli tak, nie trać czasu i zacznij uczyć się podstaw Kubernetesa.


baner

Artykuł został pierwotnie opublikowany na softwarebrothers.co.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://justjoin.it/blog/tygodnie-kubernetesem-produkcji" order_type="social" width="100%" count_of_comments="8" ]