Flux CD: Automatyczne aktualizacje aplikacji (Demo)

Czym jest Flux CD? Jest to bardzo fajne narzędzie, które pozwala na utrzymywanie aplikacji zgodnie z podejściem GitOps. GitOps jest frameworkiem operacyjnym. Jednym z jego głównych założeń jest automatyzacja, której centralnym punktem jest repozytorium kodu, które traktuje się jako źródło prawdy. Jest to możliwe dzięki wdrożeniu IaC (Infrastructure as Code), w którym cykl zmian w infrastrukturze znacząco nie różni się od tego, który stosuje się w przypadku rozwoju oprogramowania. Daje to bardzo wymierne benefity, chociażby takie jak lepsza kontrola zmian (code review),

Flux CD wpisuje się bardzo dobrze w ten model. Za jego pomocą jesteśmy w stanie wdrożyć funkcjonalność, która pozwala na dokonywanie zmian w oprogramowaniu bez konieczności wychodzenia z IDE aby wykonać deployment. Flux wykonuje to za nas.

Powyższy schemat opiera się na założeniu, że posiadamy wdrożone manifesty aplikacji oraz fluxa. Aplikacja wdrożona jest w wersji: 0.0.1. Developer commituje zmiany w aplikacji do repozytorium app repo i ustawia tag 0.0.2 (schemat uproszczony bez pull requesta i code review). Nadanie taga triggeruje wykonanie się workflow w github actions – odpowiedzialne za zbudowanie, otagowanie i opublikowanie kontenera z nową wersją aplikacji. Repozytorium kontenerów (Container Registry) jest śledzone przez fluxa zgodnie ze skonfigurowanym interwałem czasu. Flux wykrywa, że pojawiła się nowa wersja (nowy tag) i uruchomiony zostaje proces image automation, który podmienia w app manifests wersję kontenera i przy użyciu komponentu image-update-controller pushuje zmiany do repozytorium. Push wyzwala akcję aktualizacji aplikacji, której pod zostaje ponownie uruchomiony z podmienioną (wyższą) wersją kontenera dockerowego.

Przygotowanie do ćwiczenia

Aby przetestować działanie fluxa będą potrzebne następujące składniki:

  • Jedno lub dwa repozytoria github.
  • Repozytorium kontenerów (Registry).
  • Personal github token
  • Workflow w github actions, który będzie budował i publikował kontener
  • Działający cluster kubernetes. Jedno lub wielowęzłowy
  • Flux zainstalowany na stacji roboczej
  • demo_app: repozytorium kodu aplikacji
  • demo_cluster: repozytorium manifestów kubernetes

Generowanie personal github token

Token generowany jest z poziomu ustawień profilu konta github.

Settings

Menu po lewej stronie. Na samym dole są ustawienia developera.

Personal access tokens -> Tokens (classic) -> Generate new token -> Generate new token (classic)

W polu Note umieszczamy wizualny identyfikator tokena, Ustawiamy expirację oraz ustawiamy uprawnienia. Wystarczjące będą w wszystkie ze scopu repo.

Po zapisaniu zmian wygenerowany zostanie token, który należy zachować. Będzie on potrzebny fluxowi do zrobienia bootstrapa.

Krok pierwszy: Tworzymy workflow budowania kontenera.

Workflow jest zaimplementowany w repozytorium kodu aplikacji: demo_app. Różni się on nieco od tego, który został przygotowany w artykule Deployment K3S z użyciem Github Actions. W tym przypadku zadanie uruchamia się po otagowania kodu w repozytorium. Wykonywane jest na natywnym runnerze githuba. Workflow możemy opublikować przed wrzuceniem kodu aplikacji, lub wraz z nim. Ważne jest z punktu widzenia artykułu, żeby utworzył się kontener z tagiem 0.0.1

Workflow zapisany jest w repozytorium w ścieżce: .github/workflows/appdeployment.yaml

on:
  push:
    tags: [ '*\.*\.*' ]

Reagujemy na zdarzenie push z tagiem w formacie semver.

- name: Read tags
  id: read
  run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT

Po pobraniu kodu, zadanie odczytuje tagi i zapisuje je do specjalnej zmiennej GITHUB_OUTPUT

- name: Login to DockerHub
  uses: docker/login-action@v2
  if: ${{ steps.read.outputs.tag }} != 'main'
  with:
    username: ${{ secrets.DOCKERHUB_LOGIN }}
    password: ${{ secrets.DOCKERHUB_PASS }}

Wykonanie tego kroku wymaga skonfigurowania w repozytorium secretów DOCKEHUB_LOGIN i DOCKERHUB_PASS, które przehowują poświadczenia do repozytorium dockera. Zadanie wykona się jeśli został wykonany push na branch main wraz z ustawionym tagiem w formacie semver.

- name: Build and push
  uses: docker/build-push-action@v3
  if: ${{ steps.read.outputs.tag }} != 'main'
  with:
    push: true
    tags: devkrolikowski/demo_app:${{ steps.read.outputs.tag }}

Zadanie publikuje kontener z tagiem zapisanym w outputach githuba. Jeśli push do main był bez taga w formacie semver – zadanie nie zostanie wykonane. Przygotowany workflow można opublikować w repozytorium na branchu main. Cały workflow znajduje się w moim repozytorium.

Krok drugi: Przygotowanie kodu aplikacji

Aplikacja demo składa się z prostego pliku index.html oraz Dockerfile. Jest opublikowana w repozytorium demo_app. Dla uproszczenia zastosowałem workflow w którym zmiany w aplikacji publikowane są od razu na branchu głównym,

<html>
    <body>
        <h1>Strona demo</h1>
        <p>wersja: 0.0.1</p>
    </body>
</html>

FROM arm64v8/nginx:latest
COPY . /usr/share/nginx/html

W swoim przypadku musiałem użyć obrazu nginxa dla platformy ARM, ponieważ będzie on musiał działać na Raspberry Pi.

Po wypchnięciu zmian do repozytorium demo_app i nadaniu taga: 0.0.1. workflow publikuje obraz kontenera na Dockerhubie.

Krok trzeci: Przygotowanie Fluxa

W tym kroku należy zainstalować program flux na swoim komputerze i za jego pomocą wykonać bootstrap fluxa na powiązanym repozytorium github oraz klastrze kubernetes, z którym jesteśmy połączeni.

Flux: instalacja CLI

Instalacja fluxa jest banalnie prosta. W systemie Windows wystarczy wykonać polecenie:

choco install flux

W przypadku innych systemów instrukcje znajdują się w dokumentacji online.

Flux: instalacja na kubernetes

W tym miejscu przechodzimy do części właściwej, w której wykonamy deployment komponentów fluxa na klastrze kubernetes. Zanim przejdziemy do wykonywania poleceń warto na chwilę się zatrzymać i przygotować do tego kroku.

Flux Bootstrap

Bootstrap jest metodą, która pozwala na uruchomienie fluxa w oparciu o różne systemy kontroli wersji, Komponenty fluxa są wieloplatformowe. Co było istotne dla mojego środowiska, są również dostępne dla architektury armv7 na której pracuje Raspberry Pi. Bootstrap potrafi zsynchronizować się z repozytoriami takimi jak:

  • AWS Code Commit
  • Azure DevOps
  • Bitbucket
  • Github
  • Gitlab

Struktura repozytorium

W dokumentacji Fluxa znajdziemy kilka propozycji jak zorganizować strukturę repozytorium, które można zaimplementować u siebie. Ja zastosowałem podejście Monorepo, ale o bardziej płaskiej strukturze.

W katalogu apps znajdują się podkatalogi dedykowane manifestom poszczególnych aplikacji.

W katalogu clusters/demo_cluster zbootstrapowany jest flux. Nie trzeba tworzyć ręcznie struktury katalogów w repozytorium. Do wykonania bootstrapa wystarczy czyste repozytorium.

Deployment fluxa

Proces bootstrapa wykonałem na systemie Windows. W przypadku innych systemów operacyjnych jest on praktycznie taki sam. Różni się tylko nieznacznie detalami takimi jak sposób tworzenia zmiennych środowiskowych czy łamania wierszy w długich poleceniach. Trzeba wziąć na to poprawkę w czasie korzystania z listingów.

$env:GITHUB_TOKEN='ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
$env:GITHUB_USER='HomeDevopsLab'

GITHUB_TOKEN zawiera wartość tokena personal, którego generowanie zostało opisane wyżej. GITHUB_USER zawiera login do konta Github lub nazwę organizacji utworzonej na Githubie, jeśli repozytorium zostało utworzone w jej ramach.

flux bootstrap github `
  --components-extra=image-reflector-controller,image-automation-controller `
  --owner=HomeDevopsLab `
  --repository=demo_cluster `
  --branch=main `
  --path=clusters/demo_cluster `
  --read-write-key `
  --personal

Powyższe polecenie wykonuje deployment fluxa na klastrze kubernetes z jakim jesteśmy połączeni na stacji roboczej, oraz wykonuje zmiany w repozytorium. Aby uruchomić automatykę odpowiedzialną za aktualizację wersji kontenerów aplikacyjnych użyta została opcja –components-extra w której zostały podane dwa potrzebne do tego celu komponenty.

  • image-reflector-controller: skanuje repozytorium obrazów i wysyła do kubernetes metadane na temat znalezionych obrazów kontenerów. Na podstawie tych metadanych można określić czy wersja kontenera z aplikacją uległa zmianie i która jest najnowsza.
  • image-automation-controller: aktualizuje manifest deploymentu i ustawia w nim tag kontenera, który zostanie uznany jako najnowszy. Zmiany commituje do repozytorium, co powoduje wykonanie w kubernetes aktualizacji poda z aplikacją.

Krok czwarty: Deployment aplikacji na kubernetes z użyciem fluxa

Podstawowe manifesty

Tak jak opisałem wyżej, manifesty aplikacji są umieszczona w repozytorium w katalogu apps. Każdy rodzaj manifestu umieszczony jest w osobnym pliku. Aplikacja działa w namespace default. Wszystkie znajdziecie w repozytorium związanym z artykułem.

Jeśli będziecie korzystać z repozytorium, zalecam wdrożenie aplikacji bez manifestów: policy oraz registry przy pierwszym deploymencie aplikacji i podążanie za informacjami w artykule.

Deployment

Zdefiniowane zostały typowe dla kubernetes rzeczy takie jak liczba replik, specyfikacja kontenera oraz limity.

spec:
  containers:
  - name: demo-app
    image: devkrolikowski/demo_app:0.0.1 # {"$imagepolicy": "flux-system:demo-app"}

W specyfikacji kontenera, klucz image posiada dodatkową konfigurację potrzebną do tego aby Image Automation Controller wiedział jak należy wykonać aktualizację wersji aplikacji.

Service

spec:
  type: ClusterIP
  selector:
    app: demo-app
  ports:
  - port: 80
    targetPort: 80

Serwis typu ClusterIP, który łączy się do poda w którym na porcie 80 działa nginx

Ingress

K3S ma wbudowany ingress: Traefik, Więcej przykładów znajduje się w dokumentacji do Traefika.

spec:
  rules:
    - host: demo-app.lan
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name:  demo-app
                port:
                  number: 80

Jeśli do ingressa dotrze żądanie z ustawionym nagłówkiem host: demo-app.lan prześlij go do serwisu: demo_app na port 80.

Dodatkowa konfiguracja

Aby Flux wiedział, że manifesty aplikacji znajdują się w katalogu apps, stworzyłem plik w ścieżce clusters/demo_cluster/apps.yaml z taką konfiguracją jak niżej. Flux będzie skanował co minutę, czy pojawiło się coś do wdrożenia.

---
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: apps
  namespace: flux-system
spec:
  interval: 1m0s
  sourceRef:
    kind: GitRepository
    name: flux-system
  path: ./apps
  prune: true
  wait: true
  timeout: 1m0s

W tym momencie można opublikować zmiany w repozytorium demo_cluster i poczekać około minuty na wdrożenie aplikacji demo_app.

Po chwili oczekiwania aplikacja jest już wdrożona zgodnie ze specyfikacją.

Krok piąty: Wdrożenie Image Automation

ImageUpdateAutomation

Konfiguracja komponentu image-automation-controller, który jest odpowiedzialny za aktualizowanie aplikacji. W repozytorium plik znajduje się w ścieżce: clusters/demo_cluster/flux-system-automation.yaml

---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
  name: flux-system
  namespace: flux-system
spec:
  git:
    checkout:
      ref:
        branch: main
    commit:
      author:
        email: fluxcdbot@users.noreply.github.com
        name: fluxcdbot
      messageTemplate: '{{range .Updated.Images}}{{println .}}{{end}}'
    push:
      branch: main
  interval: 1m0s
  sourceRef:
    kind: GitRepository
    name: flux-system
  update:
    path: ./apps
    strategy: Setters

Polecenie generujące plik:

flux create image update flux-system `
--git-repo-ref=flux-system `
--git-repo-path="./clusters/demo_cluster" `
--checkout-branch=main `
--push-branch=main `
--author-name=fluxcdbot `
--author-email=fluxcdbot@users.noreply.github.com `
--commit-template="{{range .Updated.Images}}{{println .}}{{end}}" `
--export > ./clusters/demo_cluster/flux-system-automation.yaml

ImageRepository

---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
  name: demo-app
  namespace: flux-system
spec:
  image: devkrolikowski/demo_app
  interval: 1m0s

Instruujemy image-reflector, gdzie ma szukać nowych wersji aplikacji oraz jak często. Plik w repozytorium zapisany jest wraz z pozostałymi manifestami aplikacji w katalogu: apps/demo_app. Więcej informacji na temat ImageRepository w dokumentacji.

Polecenie generujące plik:

flux create image repository demo-app `
--image=devkrolikowski/demo_app `
--interval=1m `
--export > ./apps/demo_app/demo_app-registry.yaml

ImagePolicy

---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: demo-app
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: demo-app
  policy:
    semver:
      range: '>=0.0.1'

Definiujemy co powinno być traktowane jako nowa wersja aplikacji. W tym przypadku korzystamy ze standardu semver i określamy, że wszystkie tagi będące większe bądź równe niż 0.0.1 są traktowane jako wersje aplikacji. Dowiązujemy utworzoną politykę do definicji ImageRepository aby było wiadomo, do którego repozytorium obrazów się ona odnosi. Ten plik również znajduje się tam gdzie manifesty aplikacji: apps/demo_app. Więcej przykładów do ImagePolicy znajduje się w dokumentacji.

Polecenie generujące plik:

flux create image policy demo-app `
--image-ref=demo-app `
--select-semver='>=0.0.1' `
--export > ./apps/demo_app/demo_app-policy.yaml

Wdrożenie

Wypychamy gotowe pliki do repozytorium demo_cluster.

Obserwacje

Image Reflector zidentyfikował dostępne tagi

Zidentyfikowana została najnowsza wersja aplikacji

Krok szósty: Aktualizujemy aplikację demo_app

Z dalszymi działaniami wracamy do repozytorium demo_app. Zmiany będą polegały na dodaniu obrazka oraz aktualizacji informacji o numerze wersji.

<html>
    <body>
        <h1>Strona demo</h1>
        <h2>Raspberry Pi Logo</h2>
        <img src="img/raspberrry_pi_logo.png" width="60%">
        <p>wersja: 0.1.2</p>
    </body>
</html>

Po zapisaniu zmian publikujemy je na branchu main i nadajemy taga: 0.1.2. Spowoduje to uruchomienie workflow, który opublikuje nową wersję kontenera w dockerhubie.

Image reflecktor zaraportował, że znalazł dwa tagi w repozytorium dockerhub

Image automation poinformował, że wykonał commit i push do repozytorium na brach main. Zwróćmy uwagę, że zmienił się numer wersji (tag) obrazu kontenera.

Od tego momentu rozpoczyna się wdrożenie aplikacji, Wdrożona aplikacja wygląda zgodnie z oczekiwaniami. Odbyło się to automatycznie, po opublikowaniu i otagowaniu zmian w repozytorium aplikacji. Cała operacja trwała ok 2 min. Oczywiście było to bardzo proste wdrożenie i budowanie kontenera trwało bardzo szybko.

Można zweryfikować w repozytorium zmiany wykonane przez fluxa

Sprawdzenie stanu kontenera

Podsumowanie

Tak może wyglądać proste wdrożenie narzędziem Flux CD do implementowania zmian w aplikacji działającej na kubernetes. Ten artykuł jest jedynie wierzchołkiem góry lodowej. W dokumentacji do fluxa znajduje się jeszcze kilka ciekawych implementacji technologii takich jak HELM czy SOPS. W przyszłych artykułach będę jeszcze krążył wokół tych zagadnień.