Wdrażanie aplikacji w kilku klastrach Kubernetes z Helm

Jak Dailymotion wykorzystuje Kubernetes: wdrażanie aplikacji

W Dailymotion zaczęliśmy używać Kubernetes w produkcji 3 lata temu. Wdrażanie aplikacji w kilku klastrach to nie lada wyzwanie, dlatego w ostatnich latach staraliśmy się ulepszać nasze narzędzia i procesy robocze.

Od czego to się zaczęło

Tutaj opowiemy, jak wdrażamy nasze aplikacje na kilku klastrach Kubernetes na całym świecie.

Aby wdrożyć kilka obiektów Kubernetes jednocześnie, używamy Helm, a wszystkie nasze chart'y są przechowywane w jednym repozytorium git. Aby wdrożyć kompletny stos aplikacji z kilku usług, używamy tzw. chart'a zbiorczego. W zasadzie jest to chart, który ogłasza zależności i pozwala zainicjować API oraz jego usługi jedną komendą.

Dodatkowo napisaliśmy mały skrypt Pythona na bazie Helm, aby wykonywać kontrole, tworzyć chart'y, dodawać sekrety i wdrażać aplikacje. Wszystkie te zadania są realizowane na centralnej platformie CI z użyciem obrazu docker.

Przejdźmy do sedna.

Uwaga. Kiedy to czytacie, pierwszy kandydat do wydania Helm 3 został już ogłoszony. Główna wersja zawiera szereg ulepszeń, mających na celu rozwiązanie niektórych problemów, z którymi zmagaliśmy się w przeszłości.

Workflow rozwijania chart'ów

Dla aplikacji używamy gałęzi i postanowiliśmy zastosować to podejście do chart'ów.

  • Gałąź dev jest używana do tworzenia chart'ów, które będą testowane na klastrach deweloperskich.
  • Gdy pull request jest przesyłany do master, są one testowane na stagingu.
  • Na koniec tworzymy pull request, aby przekazać zmiany do gałęzi prod i zastosować je w produkcji.

Każde środowisko ma swoje prywatne repozytorium, które przechowuje nasze chart'y i używamy Chartmuseum z bardzo przydatnymi API. Dzięki temu zapewniamy ścisłą izolację między środowiskami oraz weryfikację chart'ów w rzeczywistych warunkach, zanim użyjemy ich w produkcji.

Repozytoria chart'ów w różnych środowiskach

Warto zauważyć, że gdy deweloperzy przesyłają gałąź dev, wersja ich chart'a jest automatycznie przesyłana do dev Chartmuseum. W ten sposób wszyscy deweloperzy używają jednego repozytorium dev i należy dokładnie podać swoją wersję chart'a, aby przypadkowo nie wykorzystać czyichś zmian.

Co więcej, nasz mały skrypt Python sprawdza obiekty Kubernetes zgodnie ze specyfikacjami Kubernetes OpenAPI za pomocą Kubeval, zanim zostaną opublikowane w Chartmusem.

Ogólny opis przepływu prac związanych z rozwojem wykresu

  1. Konfiguracja zadań pipeline zgodnie ze specyfikacją gazr.io do kontroli jakości (lint, test jednostkowy).
  2. Wysyłanie obrazu docker z narzędziami Python, które wdrażają nasze aplikacje.
  3. Konfiguracja środowiska według nazwy gałęzi.
  4. Sprawdzanie plików yaml Kubernetes za pomocą Kubeval.
  5. Automatyczne zwiększenie wersji wykresu i jego nadrzędnych wykresów (wykresów, które zależą od modyfikowanego wykresu).
  6. Wysyłanie wykresu do Chartmuseum, które odpowiada jego środowisku

Zarządzanie różnicami w klastrach

Federacja klastrów

Był czas, kiedy używaliśmy federacji klastrów Kubernetes, gdzie można było ogłaszać obiekty Kubernetes z jednego punktu końcowego API. Ale pojawiły się problemy. Na przykład, niektóre obiekty Kubernetes nie mogły być tworzone w punkcie końcowym federacji, co utrudniało zarządzanie połączonymi obiektami i innymi obiektami dla poszczególnych klastrów.

Aby rozwiązać problem, zaczęliśmy zarządzać klastrami niezależnie, co znacząco uprościło proces (użyto pierwszej wersji federacji; w drugiej coś mogło się zmienić).

Geograficznie rozproszona platforma

Obecnie nasza platforma jest rozproszona na 6 regionów — 3 lokalne i 3 w chmurze.


Rozproszone wdrożenie

Globalne wartości Helm

4 globalne wartości Helm pozwalają określić różnice między klastrami. Dla wszystkich naszych wykresów obowiązują minimalne wartości domyślne.

global:
  cloud: True
  env: staging
  region: us-central1
  clusterName: staging-us-central1

Globalne wartości

Te wartości pomagają określić kontekst dla naszych aplikacji i są używane do różnych zadań: monitorowania, śledzenia, logowania, wykonywania zewnętrznych wywołań, skalowania itd.

  • «cloud»: mamy hybrydową platformę Kubernetes. Na przykład, nasze API jest wdrażane w strefach GCP oraz w naszych centrach danych.
  • «env»: niektóre wartości mogą się zmieniać dla środowisk nienadających się do pracy. Na przykład, definicje zasobów i konfiguracje automatycznego skalowania.
  • «region»: ta informacja pomaga określić lokalizację klastra i może być używana do ustalania najbliższych punktów końcowych dla zewnętrznych usług.
  • «clusterName»: jeśli i kiedy chcemy określić wartość dla pojedynczego klastra.

Oto konkretny przykład:

{{
/* Zwraca liczby replik Horyzontalnego Autoskalera dla GraphQL */}}
{{- define "graphql.hpaReplicas" -}}
{{- if eq .Values.global.env "prod" }}
{{- if eq .Values.global.region "europe-west1" }}
minReplicas: 40
{{- else }}
minReplicas: 150
{{- end }}
maxReplicas: 1400
{{- else }}
minReplicas: 4
maxReplicas: 20
{{- end }}
{{- end -}}

Przykład szablonu Helm

Ta logika jest zdefiniowana w pomocniczym szablonie, aby nie zaśmiecać YAML Kubernetes.

Deklaracja aplikacji

Nasze narzędzia do wdrażania opierają się na kilku plikach YAML. Poniżej znajduje się przykład, jak deklarujemy usługę i jej topologię skalowania (liczbę replik) w klastrze.

releases:
  - foo.world

foo.world:                # Nazwa wersji
  services:               # Lista aplikacji/projektów dailymotion
    foobar:
      chart_name: foo-foobar
      repo: git@github.com:dailymotion/foobar
      contexts:
        prod-europe-west1:
          deployments:
            - name: foo-bar-baz
              replicas: 18
            - name: another-deployment
              replicas: 3

Definicja usługi

To schemat wszystkich kroków, które definiują nasz proces wdrażania. Ostatni krok wdraża aplikację jednocześnie na kilku klastrach roboczych.


Kroki wdrażania w Jenkinsie

A sekrety?

Jeśli chodzi o bezpieczeństwo, śledzimy wszystkie sekrety z różnych miejsc i przechowujemy je w unikalnym magazynie Vault w Paryżu.

Nasze narzędzia do wdrażania pobierają wartości sekretów z Vault, a kiedy nadchodzi czas wdrożenia, wstawiają je do Helm.

Aby to zrobić, zdefiniowaliśmy mapowanie między sekretami w Vault a sekretami, które są potrzebne naszym aplikacjom:

secrets:                                                                                                                                                                                                        
     - secret_id: "stack1-app1-password"                                                                                                                                                                                  
       contexts:                                                                                                                                                                                                   
         - name: "default"                                                                                                                                                                                         
           vaultPath: "\/kv\/dev\/stack1\/app1\/test"                                                                                                                                                               
           vaultKey: "password"                                                                                                                                                                                    
         - name: "cluster1"                                                                                                                                                                           
           vaultPath: "\/kv\/dev\/stack1\/app1\/test"                                                                                                                                                               
           vaultKey: "password"

  • Określiliśmy ogólne zasady, których należy przestrzegać przy zapisywaniu sekretów w Vault.
  • Jeśli sekret dotyczy określonego kontekstu lub klastra, należy dodać konkretny wpis. (Tutaj kontekst cluster1 ma własną wartość dla sekretu stack-app1-password).
  • W przeciwnym razie używana jest wartość domyślnie.
  • Dla każdego punktu na tej liście w secrecie Kubernetes wstawiana jest para klucz-wartość. Dlatego szablon sekretu w naszych chartach jest bardzo prosty.

apiVersion: v1
data:
{{- range $key,$value := .Values.secrets }}
  {{ $key }}: {{ $value | b64enc | quote }}
{{ end }}
kind: Secret
metadata:
  name: "{{ .Chart.Name }}"
  labels:
    chartVersion: "{{ .Chart.Version }}"
    tillerVersion: "{{ .Capabilities.TillerVersion.SemVer }}"
type: Opaque

Problemy i ograniczenia

Praca z wieloma repozytoriami

Obecnie oddzielamy rozwój chartów i aplikacji. Oznacza to, że deweloperzy muszą pracować w dwóch repozytoriach git: jednej dla aplikacji, a drugiej dla określenia jej wdrożenia w Kubernetes. 2 repozytoria git to 2 procesy robocze, a nowicjusz łatwo może się pogubić.

Zarządzanie uogólnionymi chartami jest kłopotliwe

Jak już wspomniano, uogólnione charty są bardzo wygodne do definiowania zależności i szybkiego wdrażania wielu aplikacji. Ale używamy --reuse-values, aby uniknąć przesyłania wszystkich wartości za każdym razem, gdy wdrażamy aplikację, będącą częścią tego uogólnionego charta.

W procesie nieprzerwanego dostarczania mamy tylko dwa regularnie zmieniające się parametry: liczbę replik i tag obrazu (wersję). Inne, bardziej stabilne wartości, zmieniają się ręcznie, co jest dość trudne. Co więcej, jeden błąd w wdrożeniu ogólnego szablonu może prowadzić do poważnych awarii, jak na własnym doświadczeniu się przekonaliśmy.

Aktualizacja kilku plików konfiguracyjnych

Kiedy programista dodaje nową aplikację, musi zmienić kilka plików: deklarację aplikacji, listę sekretów, dodanie aplikacji do zależności, jeśli wchodzi w skład ogólnego szablonu.

Uprawnienia Jenkins są zbyt szerokie w Vault

Obecnie mamy jednego AppRole, który odczytuje wszystkie sekrety z Vault.

Proces wycofania nie jest zautomatyzowany

Aby przeprowadzić wycofanie, trzeba wykonać polecenie na kilku klastrach, co wiąże się z ryzykiem błędów. Wykonujemy tę operację ręcznie, aby zapewnić poprawne wskazanie identyfikatora wersji.

Idziemy w kierunku GitOps

Naszym celem

Chcemy zwrócić szablon do repozytorium aplikacji, które wdraża.

Proces roboczy będzie taki sam jak podczas rozwoju. Na przykład, gdy gałąź jest wysyłana do głównej, wdrożenie będzie uruchamiane automatycznie. Główna różnica między tym podejściem a obecnym procesem roboczym polega na tym, że wszystko będzie zarządzane w git (sama aplikacja i sposób jej wdrażania w Kubernetes).

Zalet jest kilka:

  • O wiele jaśniejsze dla programisty. Łatwiej nauczyć się stosować zmiany w lokalnym szablonie.
  • Definicję wdrożenia usługi można określić tam, gdzie kod usługi.
  • Zarządzanie usuwaniem ogólnych szablonów. Usługa będzie miała swoją wersję Helm. Pozwoli to zarządzać cyklem życia aplikacji (wycofanie, aktualizacja) na najmniejszym poziomie, aby nie dotknąć innych usług.
  • Zalety gita w zarządzaniu szablonami: wycofanie zmian, dziennik audytu itp. Jeśli trzeba cofnąć zmianę szablonu, można to zrobić za pomocą gita. Wdrożenie uruchamia się automatycznie.
  • Można pomyśleć o udoskonaleniu procesu rozwoju za pomocą takich narzędzi jak Skaffold, z którym programiści mogą testować zmiany w kontekście zbliżonym do produkcji.

Dwufazowa migracja

Nasi deweloperzy korzystają z tego przepływu pracy od 2 lat, dlatego potrzebujemy maksymalnie bezbolesnej migracji. Dlatego zdecydowaliśmy się dodać pośredni etap na drodze do celu.
Pierwszy etap jest prosty:

  • Zachowujemy podobną strukturę do konfiguracji wdrażania aplikacji, ale w jednym obiekcie o nazwie DailymotionRelease.

apiVersion: "v1"
kind: "DailymotionRelease"
metadata:
  name: "app1.ns1"
  environment: "dev"
  branch: "mybranch"
spec:
  slack_channel: "#admin"
  chart_name: "app1"
  scaling:
    - context: "dev-us-central1-0"
      replicas:
        - name: "hermes"
          count: 2
    - context: "dev-europe-west1-0"
      replicas:
        - name: "app1-deploy"
          count: 2
  secrets:
    - secret_id: "app1"
      contexts:
        - name: "default"
          vaultPath: "\/kv\/dev\/ns1\/app1\/test"
          vaultKey: "password"
        - name: "dev-europe-west1-0"
          vaultPath: "\/kv\/dev\/ns1\/app1\/test"
          vaultKey: "password"

  • 1 wydanie na aplikację (bez ogólnych chartów).
  • Charty w repozytorium git aplikacji.

Rozmawialiśmy ze wszystkimi deweloperami, więc proces migracji już się rozpoczął. Pierwszy etap wciąż jest kontrolowany za pomocą platformy CI. Wkrótce napiszę kolejny post o drugim etapie: jak przeszliśmy na przepływ pracy GitOps z Flux. Opowiem, jak wszystko skonfigurowaliśmy i z jakimi trudnościami się spotkaliśmy (kilka repozytoriów, sekrety itd.). Śledźcie nas na bieżąco.

Tutaj staraliśmy się opisać nasz postęp w procesie wdrażania aplikacji przez ostatnie lata, co doprowadziło do przemyśleń na temat podejścia GitOps. Jeszcze nie osiągnęliśmy celu i będziemy informować o wynikach, ale teraz jesteśmy przekonani, że dobrze zrobiliśmy, decydując się na uproszczenie i przybliżenie do nawyków deweloperów.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster