CI/CD pipeline z użyciem Kubernetesa, AWS, Azure i .NET Core – Helm 3.0 i ACR

Opublikowane przez admin w dniu

Cześć!

Jest to następna część serii o CI/CD z użyciem Kubernetesa. W pierwszej części postawiliśmy klaster testowy w chmurze AWS. W tym poście poruszymy temat menadżera pakietów dla Kubernetesa – Helm.

Seria CI/CD pipeline z użyciem Kubernetesa, AWS, Azure i .NET Core:

  1. Stawianie klastra na AWS
  2. Helm 3.0 i ACR
  3. Dodawanie klastra w Octopus Deploy
  4. Deploy na klaster testowy
  5. Stawianie klastra na Azure oraz deploy

Przegląd architektury

Wprowadzenie

W większości przypadków nasza aplikacja w Kubernetesie będzie składać się z kilku plików yaml. W poprzednim wpisie do deployu potrzebowaliśmy dwóch takich plików – jednego dla poda i jednego dla serwisu. Główną zaletą Helma jest umożliwienie deployowania aplikacji w Kubernetesie jako pojedynczej paczki i traktowanie jej jako całości. Taką zdeployowaną paczką możemy łatwo zarządzać, wycofywać ją, itd.

Helm umożliwia tworzenie generycznych szablonów, które możemy parametryzować i podmieniać te parametry podczas deployu.

Oprócz klasycznego deployu pojedynczej aplikacji, Helm może bardzo dobrze nadawać się do szybkiego deployowania całych środowisk testowych, zawierających bazy danych, kolejki, itp.

Instalacja

Release v3.0.0 można sciągnąć stąd:

https://github.com/helm/helm/releases/tag/v3.0.0

lub poprzez choco:

choco install kubernetes-helm

Pierwszy własny szablon (chart)

Po instalacji Helma przejdźmy do stworzenia naszego pierwszego szablonu (chartu).

Aby to zrobić, wystarczy uruchomić polecenie:

helm create nazwaChartu

Zostanie stworzona następująca struktura katalogów:

Krótki opis plików

  • Chart.yaml zawiera opis naszego chartu, można tam znaleźć takie informacje, jak np. nazwę, wersje i opis. Dodatkowo, możemy w nim definiować zewnętrzne zależności naszej aplikacji, tj. bazy danych, czy inne aplikacje.
  •  Katalog Charts będzie zawierać zależności w postaci szablonów, które zostały zdefiniowane w pliku Chart.yaml. Aby je pobrać, musimy uruchomić komendę `helm dependency update`. Więcej o samych zależnościach tutaj.
  • Values.yaml zawiera domyślne wartości które zostaną podmienione w naszych szablonach, jeśli nie nadpiszemy ich przy deployu.
  • deployment,ingress,service,`serviceaccount` są przykładami użycia szablonów, w które możemy podstawiać wartości.
  • NOTES.txt – jego zawartość zostanie wypisana po instalacji aplikacji. Można tam umieścić informacje, np. jak używać naszej aplikacji. Możemy tam także używać placeholderów, które także zostaną wypełnione wartościami z pliku values.yaml lub nadpisane przy instalacji.
  • _helpers.tpl znajdują się funkcje pomocnicze, których możemy używać w naszych szablonach. Przykład takiej funkcji to generowanie aktualnej daty w metadanych poda podczas deployu. Możemy także definiować własne funkcje. Więcej o tym tutaj.
  • Katalog ‘tests’ zawiera definicje podów, które pełnią rolę testów.  Mogą one testować, czy nasza aplikacja jest dostępna po deployu.

Możemy teraz usunąć wszystkie pliki z katalogu ‘templates’, ponieważ zdefiniujemy własny szablon dla naszej aplikacji.

Do deployu aplikacji potrzebny nam będzie obiekt Deployment, który umieścimy w katalogu templates. Jest to klasyczny Deployment Kubernetesa, w który wstawione są placeholdery dla wartości, które chcemy parametryzować:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: itdepends-deployment
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: itdepends-app
  template:
    metadata:
      labels:
        app: itdepends-app
    spec:
      containers:
      - name: {{ .Values.containerName }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        envFrom:
          - secretRef:
                name: itdepends-secret  
        ports:
        - name: {{ .Values.ports.name }}
          containerPort:  {{ .Values.ports.containerPort }}
       

Aby Helm wypełnił nasz szablon wartościami, musimy naszą wartość oznaczyć podwójnymi klamrami “{{“ “}}“. Helm posiada kilka obiektów, które możemy użyć w szablonach. W tym szablonie użyłem tylko obiektu .Values, czyli tak naprawdę wartości zdefiniowanych w pliku Values.yaml. Helm posiada jeszcze inne wbudowane obiekty tj. obiekt Release, który posiada nazwę naszego releasu, jego namespace itd. Więcej o wbudowanych obiektach Helma tutaj. W tej części szablonu warto zwrócić uwagę na placeholder `{{ .Values.image.tag }}`. Będzie on kluczowy w naszym pipelinie CI/CD, ponieważ dzięki niemu będziemy mogli nadpisywać tag obrazu aplikacji, który będziemy deployować. Dodatkowo, Deployment posiada także zmienne środowiskowe (sekcja `envFrom`), które zostaną pobrane z secretów opisanych poniżej. Dzięki nim w naszej aplikacji .NET Core będziemy mogli nadpisywać wartości w pliku appsettings.json w zależności od środowiska. Dla przykładu, będziemy mogli nadpisać connection string do bazy danych w zależności czy aplikacja jest deployowana na testowe środowisko, czy na produkcyjne.

Następną częścią szablonu w katalogu templates będzie obiekt Secret.

apiVersion: v1
kind: Secret
metadata:
  name: itdepends-secret
type: Opaque
data:
  mongo__ConnectionString: {{ .Values.settings.mongoConnectionString | quote | b64enc }}
  mongo__DatabaseName: {{ .Values.settings.mongoDatabaseName| quote | b64enc }}

Jest to prosty obiekt zawierający wrażliwe dane naszej aplikacji. Do naszych placeholderów możemy także dodawać różne parametry, które przetworzą naszą wartość. Parametr quote wstawi naszą wartość w cudzysłowy, natomiast parametr `b64enc` ‘zakoduje’ tekst do base64.

Plik appsettings.jsonnaszej aplikacji .NET Core, którego wartości zostaną nadpisane powyższymi secretami wygląda następująco:

{
  "mongo": {
    "ConnectionString": "mongodb://localhost",
    "DatabaseName": "helm-test"
  },
  "localConfig":"from appSettings"
}

Warto zwrócić uwagę na to, że jedno wcięcie w powyższym jsonie odpowiada jednemu operatorowy `__` w nazwie zmiennej w secretach.

Ostatnim obiektem będzie obiekt Service, który wystawi naszą aplikacje na zewnątrz klastra:

apiVersion: v1
kind: Service
metadata:
  name: {{ .Values.service.name }}
spec:
  ports:
  - port: {{ .Values.service.port }}
    targetPort: {{ .Values.service.targetPort }}
    protocol: TCP
  selector:
    app: {{ .Values.service.appSelector }}
  type: LoadBalancer

Plik Chart.yaml będzie zawierał kilka istotnych informacji, takich jak nazwa, czy wersja aplikacji i szablonu:

apiVersion: v2
appVersion: "1.0"
description: A Helm chart for itdepends app
name: itdepends
version: 1.0.0

Ostatnim plikiem w szablonie będzie plik Values.yaml, który posiada domyślne wartości dla placeholderów:

replicaCount: 3
containerName: itdepends-container

image:
  repository: ddziub/k8s.helm.test
  tag: latest
  pullPolicy: Always

ports:
  name: itdepends-port
  containerPort: 80

service:
  name: itdepends-service
  port: 80
  targetPort: itdepends-port
  appSelector: itdepends-app

settings:
  mongoConnectionString: default conString from helm
  mongoDatabaseName: default db name from helm

Nie ma tutaj nic szczególnego. W plikach szablonu odwołujemy się do powyższych wartości poprzez następujący wzór {{.Values.NazwaObiektu.NazwaZmiennej}}. Możemy teraz sprawdzić, czy nasz szablon zawiera jakieś błędy następującą komendą:

helm lint

Jeśli wszystko poszło zgodnie z planem, możemy wypchnąć cały szablon do repozytorium na gitubie.

Testowy deploy lokalnego szablonu

Dla testu możemy zdeployować nasz szablon w klastrze. W pierwszym kroku musimy lokalnie spakować nasz szablon komendą:

helm package nazwaKatalogu

Powyższa komenda spakuje nasz szablon do zwykłego archiwum o nazwie: `nazwaKatalogu-wersja.tgz`.

Do lokalnego deployu użyję klastra minikube. Aby zdeploywać lokalną paczkę uruchamiamy komendę:

helm install test-deploy .\itdepends-1.0.0.tgz

W moim przypadku używając klastra w minikube, aby wystawić serwis lokalnie trzeba dodatkowo użyć komendy:

minikube service itdepends-service

Sparametryzowana aktualizacja deployu

Powyższy deploy użył domyślnej wartości dla tagu obrazu dockerowego – latest. Aplikacja pod tagiem dev posiada “funkcjonalność” wypisywania zmiennych z pliku appsettings.json na stronie głównej. Aby zaktualizować deploy z odpowiednim tagiem i nadpisanymi zmiennymi możemy użyć flagi --set w komendzie helm upgrade.

helm upgrade test-deploy .\itdepends-1.0.0.tgz  
--set=image.tag=dev,settings.mongoConnectionString="minikube constring",settings.mongoDatabaseName="minikube databaseName"

Jeśli także używasz minikube, musisz jeszcze raz uruchomić komendę minikube service itdepends-service, żeby zobaczyć zaaktualizowany deploy

Po przejściu na adres serwisu widzimy, że aplikacja została zaktualizowana, a zmienne z pliku appsettings.json nadpisane.

W następnych wpisach przy użyciu narzędzia Octopus Deploy zautomatyzujemy powyższy proces deploymentu. Powyższe szablony, obrazy i aplikacje można znaleźć w poniższych linkach:

Szablon helmowy

Aplikacja

Obrazy

Automatyzacja publikacji szablonów

Jeśli przetestowaliśmy wszystko lokalnie, możemy teraz zautomatyzować proces publikacji szablonów helmowych za pomocą Githuba, Azure Container Registry oraz Azure Devops.

Własne repozytoruim chartów w Azure

Helm pozwala na przechowywanie naszych szablonów w repozytoriach. Domyślnie w Helmie istnieje publiczne repozytorium, które zawiera najpopularniejsze aplikacje i części infrastruktury (np. serwer nginx). Wszystkie stabilne wersje aplikacji znajdują się tutaj. Oczywiście możemy tworzyć własne prywatne repozytoria szablonów dla naszych aplikacji. Samo repozytorium Helma to zwykly serwer http, który w zawiera opisujący je plik index.yaml oraz spakowane szablony. Plik index.yaml zawiera opisy szablonów oraz ich ścieżki. Ja do stworzenia prywatnego repozytorium użyję chmury Azure i usługi Azure Container Registry. Inne popularne rozwiązania to po prostu github lub chart museum, który umożliwia przechowywanie szablonów we wszystkich popularnych chmurach.

Aby stworzyć rejestr kontenerów Azure, przechodzimy na portal https://portal.azure.com/.

Wyszukujemy usługę `Container registries` i kilkamy Add.

Następnie wypełniamy wszystkie pola i klikamy Create. Ja wybrałem plan Basic oraz dla bezpieczeństwa warto zaznaczyć Admin user na Disable.

Tworzenie Service Principal w Azure

Do uwierzytelnienia w naszym repozytorium użyjemy service principal (nie wiem jak to dobrze spolszczyć :D). Jest to obiekt, którym możemy uwierzytelniać aplikacje, które potrzebują dostępów do innych zasobów w Azure. W tym przypadku Azure Devops (konkretnie klient Helma) będzie potrzebował dostępu do repozytorium ACR. Aby to zrobić wyszukujemy zasób Azure Active Directory i klikamy App registration -> New registration.

W następnym oknie wpisujemy nazwę aplikacji (w moim przypadku była to nazwa AzureDevops).

Po stworzeniu service principala będzie nam potrzebny jego id, tenantId oraz secret aby uwierzytelnić się w ACR. Id i tenantId znajdziemy w ekranie głównym:

Natomiast secret dla tego obiektu musimy wygenerować. Aby to zrobić klikamy zakładkę Certificates & secrets -> New client secret

Dodawanie roli Service Principal

Teraz powyższy service principal musi zostać dodany do repozytorium i subskrypcji z odpowiednią rolą. Po przejściu do poprzednio stworzonego repo przechodzimy do zakładki Access control (IAM), klikamy Add -> Add role assignment.

Następnie wyszukujemy po nazwie stworzonego service princpiala i wybieramy role AcrPush, ponieważ będziemy tym kontem publikować nasz szablon:

Ten krok powtarzamy także dla naszej subskrypcji (zasób Subscriptions) jednak nadając tylko uprawnienia typu Reader.

Po dodaniu service princpal do repozytorium i subskrypcji możemy przejść do automatyzacji publikacji szablonu w AzureDevops.

Azure Devops

Edit 8.12.2019: Łukasz Kałużny w komentarzach poradził jak można zrobić ten pipeline lepiej.  Sekcja Azure Devops zostanie poprawiona. Dzięki Łukasz!

Logujemy się w portalu używając konta w githubie  https://dev.azure.com/.

Wpisujemy nazwę projektu i określamy jego widoczność.

Następnym krokiem będzie dodanie connection dla naszego service principala. W tym celu klikamy przycisk Project settings w lewym dolnym rogu. W sekcji Pipelines wybieramy service connections -> new service connection -> Azure Resource Manager.

Aby dodać credentiale service principala musimy rozszerzyć domyślne okno dialogowe naciskając ” use the full version of the service connection dialog”. Po wypełnieniu wszystkich pól zaznaczamy checkbox “Allow all pipelines to use this connection”. Dla pewności możemy zweryfikować nasz connection.

Następnie przechodzimy do zakładki Pipelines  i klikamy  New pipelines -> Github-> Starter pipeline i wybieramy githubowe repozytorium z szablonem Helmowym.

Teraz zdefiniujemy plik azure-pipelines.yaml, który potrzebny jest do uruchomienia naszego pipeline’u w Azure Devops. Oto jego treść:

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'


steps:
- task: [email protected]
  inputs:
    helmVersionToInstall: 'latest'

- script: |
    helm lint ./itdepends
    helm package ./itdepends
  displayName: 'Packaging helm'
- task: [email protected]
  inputs:
    azureSubscription: 'HelmPublish'
    scriptType: 'bash'
    scriptLocation: 'inlineScript'
    inlineScript: |
      az acr helm repo add --name itdepends
      package="$(find -name *.tgz)"
      az acr helm push --name itdepends $package
    failOnStandardError: true

Po kolei:

  • uruchamiamy build jeśli wypchniemy coś na gałęź master
  • wirtualna maszyna na której zostanie uruchomiony build to ubuntu
  • instalujemy helma. Ten krok można wybrać z tasków:
  • uruchamiamy komendę helm lint aby sprawdzić poprawność szablonu
  • uruchamiamy komendę helm package, która spakuje nasz szablon do rozszerzenia tgz oraz wpisze w nazwie pliku wersję szablonu np. itdepends-1.0.0.tgz
  • uruchamiamy task AzureCLI. Możemy również dodać go z poziomu tasków:
    Powyższy krok został zdefiniowany w następujący sposób:

HelmPublish to nazwa naszego poprzednio dodanego connection. Dodatkowo uruchamiamy następujący skrypt:

az acr helm repo add --name itdepends
package="$(find -name *.tgz)"
az acr helm push --name itdepends $package
  • Dodajemy nasze repo.
  • Wyszukujemy pliki o rozszerzeniu .tgz (będzie tylko jeden w danym buildzie) i przypisujemy jego nazwę do zmiennej package.
  • Wypychamy paczkę do repozytorium.

Po wykonaniu powyższych kroków klikamy Save and run. Plik azure-pipelines.yaml zostanie wypchnięty do naszego repo. Po chwili rozpocznie się build szablonu. Jeśli wszystko poszło zgodnie z planem powinniśmy zobaczyć wszystkie poszczególne kroki na zielono.

W kroku AzureCLI  możemy zobaczyć że nasz szablon został wypchnięty do repozytorium.

Aby jeszcze zweryfikować czy nasza paczka istnieje w repozytorium, możemy zalogować się lokalnie w Azure poprzez AzureCLI używając naszego konta głównego (jeśli nie posiadasz AzureCLI lokalnie to, tutaj możesz je pobrać). Zalogujemy się na nie uruchamiając komendę az login. Dzięki temu zostaniemy przeniesieni do przeglądarki gdzie możemy się zalogować. Następnie dodajemy nasze repozytorium do Helma używając komendy:

az acr helm repo add --name itdepends

Wyszukujemy teraz naszą paczkę w repo komendą:

helm search repo itdepends

Teraz możemy zdeployować nasz szablon używając repozytorium. Jednak najpierw musimy usunąć lokalny deploy komendą:

helm uninstall test-deploy

i następnie:

helm install repo-test-deploy itdepends

Podsumowanie

W tym wpisie poznaliśmy Helma i jego możliwości. Zautomatyzowaliśmy także proces publikowania szablonów dla naszych aplikacji. Repozytorium Helma będzie nam potrzebne do zautomatyzowanego deployu w narzędziu Octopus Deploy, o którym opowiem w następnych wpisach.