Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Cześć! Ostatnio pojawiło się wiele świetnych narzędzi do automatyzacji, zarówno do budowania obrazów Docker, jak i do wdrażania w Kubernetes. W związku z tym postanowiłem pobawić się GitLabem, dogłębnie zbadać jego możliwości i oczywiście skonfigurować pipeline.

Inspiracją do tej pracy była strona kubernetes.io, która generowana jest z kodu źródłowego automatycznie, a na każdy przesłany pull request robot automatycznie generuje wersję podglądową strony z twoimi zmianami i udostępnia link do przeglądania.

Starałem się zbudować podobny proces od podstaw, ale całkowicie oparty na GitLab CI i wolnych narzędziach, których używam do wdrażania aplikacji w Kubernetes. Dziś w końcu opowiem Wam o nich bardziej szczegółowo.

W artykule zostaną omówione takie narzędzia jak:
Hugo, qbec, kaniko, git-crypt i GitLab CI tworzenie dynamicznych środowisk.

Spis treści

  1. Wprowadzenie do Hugo
  2. Przygotowanie Dockerfile
  3. Wprowadzenie do kaniko
  4. Wprowadzenie do qbec
  5. Testujemy Gitlab-runner z Kubernetes-executor
  6. Wdrożenie Helm-chartów z qbec
  7. Wprowadzenie do git-crypt
  8. Tworzymy obraz toolbox
  9. Nasz pierwszy pipeline i budowa obrazów według tagów
  10. Automatyzacja wdrożenia
  11. Artefakty i budowa przy push w master
  12. Dynamiczne środowiska
  13. Aplikacje przeglądowe

1. Wprowadzenie do Hugo

Jako przykład naszego projektu spróbujemy stworzyć stronę do publikacji dokumentacji, opartej na Hugo. Hugo to statyczny generator treści.

Dla tych, którzy nie znają statycznych generatorów, opowiem o nich nieco więcej. W przeciwieństwie do typowych silników stron z bazą danych i jakimś php, które generują strony „na żywo” w odpowiedzi na zapytania użytkowników, statyczne generatory działają nieco inaczej. Pozwalają one wziąć źródła, którymi zazwyczaj są zestawy plików w formacie Markdown i szablony, a następnie skompilować je w całkowicie gotową stronę.

Innymi słowy, na wyjściu otrzymasz strukturę katalogów i zestaw wygenerowanych plików html, które można łatwo przesłać na dowolny tani hosting i uzyskać działającą stronę.

Hugo można zainstalować lokalnie i wypróbować go w działaniu:

Inicjujemy nową stronę:

hugo new site docs.example.org

I od razu git-repozytorium:

cd docs.example.org
git init

Na razie nasza strona jest całkowicie czysta i aby coś na niej się pojawiło, najpierw musimy podłączyć motyw. Motyw to nic innego jak zbiór szablonów oraz określonych reguł, według których generowana jest nasza strona.

Jako temat użyjemy Learn, który, moim zdaniem, doskonale nadaje się na stronę z dokumentacją.

Chciałbym również zwrócić uwagę na to, że nie musimy przechowywać plików motywu w repozytorium naszego projektu, zamiast tego możemy po prostu podłączyć go używając git submodule:

git submodule add https://github.com/matcornic/hugo-theme-learn themes/learn

W ten sposób w naszym repozytorium będą znajdować się tylko pliki bezpośrednio związane z naszym projektem, a podłączony motyw pozostanie w postaci linku do konkretnego repozytorium i jego commita, co oznacza, że zawsze można go pobrać z oryginalnego źródła i nie obawiać się niekompatybilnych zmian.

Poprawimy konfigurację config.toml:

baseURL = "http://docs.example.org/"
languageCode = "en-us"
title = "Moja strona dokumentacyjna"
theme = "learn"

Już na tym etapie możemy uruchomić:

hugo server

A pod adresem http://localhost:1313/ możemy sprawdzić naszą nowo utworzoną stronę, wszystkie zmiany wprowadzone w katalogu automatycznie aktualizują otwartą stronę w przeglądarce, bardzo wygodne!

Spróbujmy stworzyć stronę tytułową w content/_index.md:

# My docs site

## Welcome to the docs!

You will be very smart :-)

Zrzut ekranu właśnie utworzonej strony

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Aby wygenerować stronę, wystarczy uruchomić:

hugo

Zawartość katalogu public/ będzie twoją stroną.
Tak, swoją drogą, dodajmy ją od razu do .gitignore:

echo /public > .gitignore

Nie zapominajmy o zatwierdzeniu naszych zmian:

git add .
git commit -m "Nowa strona utworzona"

2. Przygotowanie Dockerfile

Nadszedł czas, aby określić strukturę naszego repozytorium. Zwykle używam czegoś takiego:

.
├── deploy
│   ├── app1
│   └── app2
└── dockerfiles
    ├── image1
    └── image2

  • dockerfiles/ — zawierają katalogi z Dockerfile'ami i wszystkim, co potrzebne do budowy naszych obrazów dockerowych.
  • deploy/ — zawiera katalogi do wdrażania naszych aplikacji w Kubernetes

W ten sposób nasz pierwszy Dockerfile stworzymy w dockerfiles/website/Dockerfile

FROM alpine:3.11 as builder
ARG HUGO_VERSION=0.62.0
RUN wget -O- https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-64bit.tar.gz | tar -xz -C /usr/local/bin
ADD . /src
RUN hugo -s /src

FROM alpine:3.11
RUN apk add --no-cache darkhttpd
COPY --from=builder /src/public /var/www
ENTRYPOINT [ "/usr/bin/darkhttpd" ]
CMD [ "/var/www" ]

Jak możesz zauważyć, Dockerfile zawiera dwa Z FROM, ta możliwość nazywa się multi-stage build i pozwala na wykluczenie z końcowego obrazu dockerowego wszystkich niepotrzebnych elementów.
W ten sposób nasz końcowy obraz będzie zawierał tylko darkhttpd (lekkoskalowy serwer HTTP) i public/ — treść naszej statycznie wygenerowanej strony.

Nie zapominajmy o zatwierdzeniu naszych zmian:

git add dockerfiles/website
git commit -m "Dodaj Dockerfile dla strony"

3. Wprowadzenie do kaniko

Jako narzędzie do budowania obrazów docker postanowiłem użyć kaniko, ponieważ do jego działania nie jest wymagany demon docker, a sama budowa może odbywać się na dowolnej maszynie, przechowując cache bezpośrednio w rejestrze, co eliminuje potrzebę posiadania pełnoprawnego magazynu permanentnego.

Do zbudowania obrazu wystarczy uruchomić kontener z kaniko executor i przekazać mu bieżący kontekst budowy, można to zrobić też lokalnie, przez docker:

docker run -ti --rm 
  -v $PWD:\/workspace 
  -v ~\/\.docker\/config.json:\/kaniko\/\.docker\/config.json:ro 
  gcr.io\/kaniko-project\/executor:v0.15.0 
  --cache 
  --dockerfile=dockerfiles\/website\/Dockerfile 
  --destination=registry.gitlab.com\/kvaps\/docs.example.org\/website:v0.0.1

Gdzie registry.gitlab.com\/kvaps\/docs.example.org\/website — nazwa twojego obrazu docker, po zbudowaniu zostanie automatycznie wypchnięty do rejestru docker.

Parametr —cache pozwala na cache'owanie warstw w rejestrze docker, w podanym przykładzie będą one przechowywane w registry.gitlab.com\/kvaps\/docs.example.org\/website\/cache, ale możesz również wskazać inną ścieżkę za pomocą parametru —cache-repo.

Zrzut ekranu docker-registry

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

4. Wprowadzenie do qbec

Qbec — to narzędzie do wdrażania, które pozwala deklaratywnie opisywać manifesty twojej aplikacji i wdrażać je w Kubernetes. Użycie Jsonnet jako głównej składni umożliwia znaczne uproszczenie opisu różnic dla różnych środowisk, a także niemal całkowite wyeliminowanie powtarzalności kodu.

Może to być szczególnie istotne w przypadkach, gdy musisz wdrożyć aplikację w kilku klastrach z różnymi parametrami i chcesz deklaratywnie opisać je w Git.

Qbec umożliwia także renderowanie chartów Helm, przekazując im potrzebne parametry, a następnie operowanie nimi jak zwykłymi manifestami, w tym można nakładać na nie różne mutacje, co z kolei pozwala wyeliminować potrzebę użycia ChartMuseum. Można więc przechowywać i renderować charty bezpośrednio z git, gdzie naprawdę ich miejsce.

Jak wspomniałem wcześniej, wszystkie wdrożenia będziemy przechowywać w katalogu deploy/:

mkdir deploy
cd deploy

Zainicjujmy naszą pierwszą aplikację:

qbec init website
cd website

Obecnie struktura naszej aplikacji wygląda następująco:

.
├── components
├── environments
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
└── qbec.yaml

przyjrzyjmy się plikowi qbec.yaml:

apiVersion: qbec.io\/v1alpha1
kind: App
metadata:
  name: website
spec:
  environments:
    default:
      defaultNamespace: docs
      server: https:\/\/kubernetes.example.org:8443
  vars: {}

Tu interesuje nas przede wszystkim spec.environments, qbec już stworzył dla nas domyślne środowisko i wziął adres serwera oraz namespace z naszej bieżącej kubeconfig.
Teraz podczas wdrażania w default środowisko, qbec zawsze będzie wdrażać tylko w wskazany klaster Kubernetes i w wskazany namespace, więc nie będziesz musiał przełączać się między kontekstami i namespace'ami, aby przeprowadzić wdrożenie.
W razie potrzeby zawsze możesz zaktualizować ustawienia w tym pliku.

Wszystkie twoje środowiska są opisane w qbec.yaml, oraz w pliku params.libsonnet, w którym określono, skąd należy pobierać dla nich parametry.

Następnie widzimy dwa katalogi:

  • components/ — tutaj będą przechowywane wszystkie manifesty naszego aplikacji, mogą być opisane zarówno w jsonnet, jak i w zwykłych plikach yaml
  • environments/ — tutaj będziemy opisywać wszystkie zmienne (parametry) dla naszych środowisk.

Domyślnie mamy dwa pliki:

  • environments/base.libsonnet — będzie zawierał wspólne parametry dla wszystkich środowisk
  • environments/default.libsonnet — zawiera nadpisane parametry dla środowiska default

Otwórzmy environments/base.libsonnet i dodajmy tam parametry dla naszego pierwszego komponentu:

{
  components: {
    website: {
      name: 'example-docs',
      image: 'registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1',
      replicas: 1,
      containerPort: 80,
      servicePort: 80,
      nodeSelector: {},
      tolerations: [],
      ingressClass: 'nginx',
      domain: 'docs.example.org',
    },
  },
}

Stwórzmy również nasz pierwszy komponent components/website.jsonnet:

lokalne środowisko = {
  nazwa: std.extVar('qbec.io/env'),
  przestrzeń nazw: std.extVar('qbec.io/defaultNs'),
};
lokalne p = import '.. / params.libsonnet';
lokalne parametry = p.components.website;

[
  {
    apiVersion: 'apps/v1',
    kind: 'Deployment',
    metadata: {
      labels: { app: parametry.name },
      name: parametry.name,
    },
    spec: {
      replicas: parametry.replicas,
      selector: {
        matchLabels: {
          app: parametry.name,
        },
      },
      template: {
        metadata: {
          labels: { app: parametry.name },
        },
        spec: {
          containers: [
            {
              name: 'darkhttpd',
              image: parametry.image,
              ports: [
                {
                  containerPort: parametry.containerPort,
                },
              ],
            },
          ],
          nodeSelector: parametry.nodeSelector,
          tolerations: parametry.tolerations,
          imagePullSecrets: [{ name: 'regsecret' }],
        },
      },
    },
  },
  {
    apiVersion: 'v1',
    kind: 'Service',
    metadata: {
      labels: { app: parametry.name },
      name: parametry.name,
    },
    spec: {
      selector: {
        app: parametry.name,
      },
      ports: [
        {
          port: parametry.servicePort,
          targetPort: parametry.containerPort,
        },
      ],
    },
  },
  {
    apiVersion: 'extensions/v1beta1',
    kind: 'Ingress',
    metadata: {
      annotations: {
        'kubernetes.io/ingress.class': parametry.ingressClass,
      },
      labels: { app: parametry.name },
      name: parametry.name,
    },
    spec: {
      rules: [
        {
          host: parametry.domain,
          http: {
            paths: [
              {
                backend: {
                  serviceName: parametry.name,
                  servicePort: parametry.servicePort,
                },
              },
            ],
          },
        },
      ],
    },
  },
]

W tym pliku opisaliśmy trzy zasoby Kubernetes, są to: Deployment, Service i Ingress. Jeśli zechcemy, moglibyśmy je podzielić na różne komponenty, ale na tym etapie wystarczy nam jeden.

Składnia jsonnet jest bardzo podobny do zwykłego json, w zasadzie zwykły json jest już poprawnym jsonnet, więc przez jakiś czas może być łatwiej skorzystać z usług online takich jak yaml2json aby skonwertować znany wam yaml do json, lub, jeśli wasze komponenty nie zawierają żadnych zmiennych, można je opisać w formie zwykłego yaml.

Podczas pracy z jsonnet gorąco polecam zainstalować wtyczkę do waszego edytora

Na przykład dla vim jest wtyczka vim-jsonnet, która zapewnia podświetlanie składni i automatycznie wykonuje jsonnet fmt przy każdym zapisie (wymaga zainstalowanego jsonnet).

Wszystko gotowe, teraz możemy rozpocząć wdrażanie:

Aby zobaczyć co mamy, wykonamy:

qbec show default

Na wyjściu zobaczycie wyrenderowane manifesty yaml, które zostaną zastosowane w klastrze default.

Świetnie, teraz zastosujemy:

qbec apply default

Na wyjściu zawsze zobaczycie, co zostanie wykonane w waszym klastrze, qbec poprosi was o potwierdzenie zmian, wpisując y możecie potwierdzić swoje zamiary.

Gotowe, nasze aplikacja została wdrożona!

W przypadku wprowadzenia zmian zawsze możesz wykonać:

qbec diff default

aby zobaczyć, jak te zmiany wpłyną na bieżący deployment

Nie zapominajmy o zatwierdzeniu naszych zmian:

cd ../..
git add deploy/website
git commit -m "Dodaj deployment dla strony"

5. Testujemy Gitlab-runner z Kubernetes-executorem

Do niedawna używałem tylko zwykłego gitlab-runner na wcześniej przygotowanej maszynie (kontejnerze LXC) z executorami shell lub docker. Na początku mieliśmy kilka takich runnerów globalnie zdefiniowanych w naszym GitLabie. Służyły do budowania obrazów docker dla wszystkich projektów.

Jednak jak pokazała praktyka — ta opcja nie jest najlepsza, zarówno pod względem praktyczności, jak i bezpieczeństwa. Zdecydowanie lepiej i ideologicznie poprawniej jest mieć osobne runnerzy zainstalowane dla każdego projektu, a nawet dla każdego środowiska.

Na szczęście nie jest to problemem, ponieważ teraz będziemy wdrażać gitlab-runner bezpośrednio jako część naszego projektu prosto w Kubernetes.

GitLab oferuje gotowy helm-chart do wdrożenia gitlab-runner w Kubernetes. Tak więc wszystko, co musisz zrobić, to poznać registration token dla naszego projektu w Ustawienia —> CI / CD —> Runnery i przekazać go do helm:

helm repo add gitlab https://charts.gitlab.io

helm install gitlab-runner 
  --set gitlabUrl=https://gitlab.com 
  --set runnerRegistrationToken=yga8y-jdCusVDn_t4Wxc 
  --set rbac.create=true 
  gitlab/gitlab-runner

Gdzie:

  • https://gitlab.com — adres twojego serwera GitLab.
  • yga8y-jdCusVDn_t4Wxc — registration token dla twojego projektu.
  • rbac.create=true — przyznaje runnerowi niezbędne uprawnienia do tworzenia podów do wykonywania naszych zadań za pomocą kubernetes-executora.

Jeśli wszystko zostało zrobione poprawnie, powinieneś zobaczyć zarejestrowanego runnery w sekcji Runnery, w ustawieniach twojego projektu.

Zrzut ekranu dodanego runnery

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Czy to takie proste? — tak, takie proste! Żadnych więcej kłopotów z ręczną rejestracją runnerów, od tej chwili runnery będą tworzone i usuwane automatycznie.

6. Wdrożenie Helm-chartów z QBEC

Ponieważ zdecydowaliśmy się traktować gitlab-runner jako część naszego projektu, nadszedł czas na opisanie go w naszym repozytorium Git.

Moglibyśmy opisać go jako oddzielny komponent website, ale w przyszłości planujemy wdrażać różne kopie website bardzo często, w przeciwieństwie gitlab-runner, który będzie wdrożony tylko raz na każdy klaster Kubernetes. Więc zacznijmy od zainicjowania oddzielnej aplikacji dla niego:

cd deploy
qbec init gitlab-runner
cd gitlab-runner

Tym razem nie będziemy ręcznie opisywać obiektów Kubernetes, ale weźmiemy gotowy szablon Helm. Jedną z zalet qbec jest możliwość renderowania szablonów Helm bezpośrednio z repozytorium Git.

Podłączmy go używając git submodule:

git submodule add https://gitlab.com/gitlab-org/charts/gitlab-runner vendor/gitlab-runner

Teraz katalog vendor/gitlab-runner zawiera nasze repozytorium ze szablonem dla gitlab-runnera.

W podobny sposób można podłączać inne repozytoria, na przykład całe repozytorium z oficjalnymi szablonami. https://github.com/helm/charts

Opiszmy komponent components/gitlab-runner.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.gitlabRunner;

std.native('expandHelmTemplate')(
  '../vendor/gitlab-runner',
  params.values,
  {
    nameTemplate: params.name,
    namespace: env.namespace,
    thisFile: std.thisFile,
    verbose: true,
  }
)

Pierwszym argumentem do expandHelmTemplate przekazujemy ścieżkę do szablonu, następnie params.values, które weźmiemy z parametrów środowiska, a potem obiekt z

  • nameTemplate — nazwa wydania
  • namespace — przestrzeń nazw przekazywana do Helm
  • thisFile — wymagany parametr, który przekazuje ścieżkę do bieżącego pliku
  • verbose — pokazuje polecenie helm template ze wszystkimi argumentami podczas renderowania szablonu.

Teraz opiszmy parametry dla naszego komponentu w environments/base.libsonnet:

local secrets = import '../secrets/base.libsonnet';

{
  components: {
    gitlabRunner: {
      name: 'gitlab-runner',
      values: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          create: true,
        },
        runnerRegistrationToken: secrets.runnerRegistrationToken,
      },
    },
  },
}

Zwróć uwagę runnerRegistrationToken bierze się z zewnętrznego pliku secrets/base.libsonnet, stwórzmy go:

{
  runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}

Sprawdźmy, czy wszystko działa:

qbec show default

jeśli wszystko jest w porządku, możemy usunąć nasze wcześniej wdrożone wydanie przez Helm:

helm uninstall gitlab-runner

i wdrożyć je ponownie, ale już przez qbec:

qbec apply default

7. Wprowadzenie do git-crypt

Git-crypt to narzędzie, które pozwala skonfigurować przezroczyste szyfrowanie dla twojego repozytorium.

Na ten moment struktura naszego katalogu dla gitlab-runnera wygląda tak:

.
├── components
│   ├── gitlab-runner.jsonnet
├── environments
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
├── qbec.yaml
├── secrets
│   └── base.libsonnet
└── vendor
    └── gitlab-runner (submodule)

Ale przechowywanie sekretów w Git nie jest bezpieczne, prawda? Musimy je więc odpowiednio zaszyfrować.

Zazwyczaj nie zawsze ma to sens w przypadku jednej zmiennej. Możesz przekazywać sekrety do qbec i przez zmienne środowiskowe twojego systemu CI.
Warto zauważyć, że istnieją również bardziej skomplikowane projekty, które mogą zawierać znacznie więcej sekretów, a przekazanie ich wszystkich za pomocą zmiennych środowiskowych będzie niezwykle trudne.

Ponadto w takim przypadku nie mógłbym opowiedzieć ci o tym wspaniałym narzędziu, jakim jest git-crypt.

git-crypt jest również wygodne, ponieważ pozwala zachować całą historię sekretów oraz porównywać, łączyć i rozwiązywać konflikty tak samo, jak przyzwyczailiśmy się to robić w przypadku Git.

Pierwszą rzeczą po zainstalowaniu git-crypt musimy wygenerować klucze dla naszego repozytorium:

git crypt init

Jeśli masz klucz PGP, możesz od razu dodać się jako współpracownika do tego projektu:

git-crypt add-gpg-user kvapss@gmail.com

Dzięki temu zawsze będziesz mógł odszyfrować to repozytorium używając swojego klucza prywatnego.

Jeśli nie masz klucza PGP i nie przewidujesz jego uzyskania, możesz pójść inną drogą i wyeksportować klucz projektu:

git crypt export-key /path/to/keyfile

Dzięki temu każdy, kto posiada wyeksportowany keyfile będzie mógł odszyfrować twoje repozytorium.

Nadszedł czas, aby skonfigurować nasz pierwszy sekret.
Przypominam, że wciąż znajdujemy się w katalogu deploy/gitlab-runner/, gdzie mamy katalog secrets/, stwórzmy zatem plik secrets/.gitattributes z taką zawartością:

* filter=git-crypt diff=git-crypt
.gitattributes !filter !diff

Jak widać z treści, wszystkie pliki zgodne z maską * będą przetwarzane przez git-crypt, z wyjątkiem samego .gitattributes

Możemy to sprawdzić uruchamiając:

git crypt status -e

W rezultacie otrzymamy listę wszystkich plików w repozytorium, dla których aktywowane jest szyfrowanie.

I to wszystko, teraz możemy śmiało zakomitować nasze zmiany:

cd ../..
git add .
git commit -m "Add deploy for gitlab-runner"

Aby zablokować repozytorium, wystarczy wykonać:

git crypt lock

i w tym momencie wszystkie zaszyfrowane pliki zamienią się w binarne coś, czego nie będzie można odczytać.
Aby odszyfrować repozytorium, wykonać:

git crypt unlock

8. Tworzymy obraz toolbox

Obraz toolbox to obraz ze wszystkimi narzędziami, które będziemy używać do wdrożenia naszego projektu. Będzie używany przez gitlab-runnery do wykonywania standardowych zadań wdrożeniowych.

Tutaj wszystko jest proste, tworzymy nowy dockerfiles/toolbox/Dockerfile z taką zawartością:

FROM alpine:3.11

RUN apk add --no-cache git git-crypt

RUN QBEC_VER=0.10.3 
 && wget -O- https://github.com/splunk/qbec/releases/download/v${QBEC_VER}/qbec-linux-amd64.tar.gz 
     | tar -C /tmp -xzf - 
 && mv /tmp/qbec /tmp/jsonnet-qbec /usr/local/bin/

RUN KUBECTL_VER=1.17.0 
 && wget -O /usr/local/bin/kubectl 
      https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/linux/amd64/kubectl 
 && chmod +x /usr/local/bin/kubectl

RUN HELM_VER=3.0.2 
 && wget -O- https://get.helm.sh/helm-v${HELM_VER}-linux-amd64.tar.gz 
     | tar -C /tmp -zxf - 
 && mv /tmp/linux-amd64/helm /usr/local/bin/helm

Jak możesz zauważyć, w tym obrazie instalujemy wszystkie narzędzia, które wykorzystaliśmy do wdrożenia naszej aplikacji. Potrzebujemy tutaj tylko kubectl, ale być może będziesz chciał się z nim pobawić na etapie konfiguracji pipeline'u.

Aby móc komunikować się z Kubernetes i wdrażać do niego aplikację, musimy skonfigurować rolę dla podów generowanych przez gitlab-runner.

W tym celu przejdźmy do katalogu z gitlab-runnerem:

cd deploy/gitlab-runner

i dodamy nowy komponent components/rbac.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.rbac;

[
  {
    apiVersion: 'v1',
    kind: 'ServiceAccount',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
  },
  {
    apiVersion: 'rbac.authorization.k8s.io/v1',
    kind: 'Role',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
    rules: [
      {
        apiGroups: [
          '*',
        ],
        resources: [
          '*',
        ],
        verbs: [
          '*',
        ],
      },
    ],
  },
  {
    apiVersion: 'rbac.authorization.k8s.io/v1',
    kind: 'RoleBinding',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
    roleRef: {
      apiGroup: 'rbac.authorization.k8s.io',
      kind: 'Role',
      name: params.name,
    },
    subjects: [
      {
        kind: 'ServiceAccount',
        name: params.name,
        namespace: env.namespace,
      },
    ],
  },
]

Opiszmy również nowe parametry w environments/base.libsonnet, który teraz wygląda tak:

local secrets = import '../secrets/base.libsonnet';

{
  components: {
    gitlabRunner: {
      name: 'gitlab-runner',
      values: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          create: true,
        },
        runnerRegistrationToken: secrets.runnerRegistrationToken,
        runners: {
          serviceAccountName: $.components.rbac.name,
          image: 'registry.gitlab.com/kvaps/docs.example.org/toolbox:v0.0.1',
        },
      },
    },
    rbac: {
      name: 'gitlab-runner-deploy',
    },
  },
}

Zwróć uwagę $.components.rbac.name odnosi się do name dla komponentu rbac

Sprawdźmy, co się zmieniło:

qbec diff default

i zastosujmy nasze zmiany w Kubernetes:

qbec apply default

Nie zapomnij również zakommitować naszych zmian w git:

cd ../../
git add dockerfiles/toolbox
git commit -m "Dodaj Dockerfile dla toolbox"
git add deploy/gitlab-runner
git commit -m "Skonfiguruj gitlab-runner do używania toolbox"

9. Nasz pierwszy pipeline i budowa obrazów według tagów

W głównym katalogu projektu stworzymy .gitlab-ci.yml z taką zawartością:

.build_docker_image:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug-v0.15.0
    entrypoint: [""]
  before_script:
    - echo "{"auths":{"$CI_REGISTRY":{"username":"$CI_REGISTRY_USER","password":"$CI_REGISTRY_PASSWORD"}}}" > /kaniko/.docker/config.json

build_toolbox:
  extends: .build_docker_image
  script:
    - /kaniko/executor --cache --context $CI_PROJECT_DIR/dockerfiles/toolbox --dockerfile $CI_PROJECT_DIR/dockerfiles/toolbox/Dockerfile --destination $CI_REGISTRY_IMAGE/toolbox:$CI_COMMIT_TAG
  only:
    refs:
      - tags

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_TAG
  only:
    refs:
      - tags

Zauważ, że używamy GIT_SUBMODULE_STRATEGY: normal dla tych zadań, które wymagają wyraźnej inicjalizacji submodułów przed wykonaniem.

Nie zapominajmy o zatwierdzeniu naszych zmian:

git add .gitlab-ci.yml
git commit -m "Automatyzacja budowy docker"

Myślę, że można to śmiało nazwać wersją v0.0.1 i nadać tag:

git tag v0.0.1

Będziemy nadawać tagi za każdym razem, gdy będziemy potrzebować wydać nową wersję. Tagi w obrazach Docker będą powiązane z tagami Git. Każdy push z nowym tagiem zainicjuje budowę obrazów z tym tagiem.

Wykonamy git push —tags, i zobaczymy nasz pierwszy pipeline:

Zrzut ekranu pierwszego pipeline'a

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Należy zwrócić uwagę, że budowa po tagach nadaje się do budowy obrazów docker, ale nie nadaje się do wdrażania aplikacji w Kubernetes. Ponieważ nowe tagi mogą być przypisywane do starych commitów, w takim przypadku inicjalizacja pipeline'a dla nich doprowadzi do wdrożenia starej wersji.

Aby rozwiązać ten problem, zazwyczaj budowa obrazów docker jest powiązana z tagami, a wdrożenie aplikacji z gałęzią master, w której są zakodowane wersje zbudowanych obrazów. Tylko w tym przypadku będziesz mógł zainicjować rollback za pomocą prostego revert master-gałęzi.

10. Automatyzacja wdrożenia

Aby Gitlab-runner mógł odszyfrować nasze sekrety, musimy wyeksportować klucz repozytorium i dodać go do zmiennych środowiskowych naszego CI:

git crypt export-key /tmp/docs-repo.key
base64 -w0 /tmp/docs-repo.key; echo

uzyskaną linię zapiszemy w Gitlab, w tym celu przejdziemy do ustawień naszego projektu:
Ustawienia —> CI / CD —> Zmienne

I stworzymy nową zmienną:

Type
Klucz
@Value
Chroniona
Ukryta
Zakres

Plik
GITCRYPT_KEY
<your string>
true (na czas nauki można i false)
true
Wszystkie środowiska

Zrzut ekranu dodanej zmiennej

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Teraz zaktualizujemy nasz .gitlab-ci.yml dodając do niego:

.deploy_qbec_app:
  stage: deploy
  only:
    refs:
      - master

deploy_gitlab_runner:
  extends: .deploy_qbec_app
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  before_script:
    - base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
  script:
    - qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --wait --yes

deploy_website:
  extends: .deploy_qbec_app
  script:
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes

Wprowadziliśmy kilka nowych opcji dla qbec:

  • —root some/app — pozwala określić katalog konkretnej aplikacji
  • —force:k8s-context __incluster__ — to magiczna zmienna, która informuje, że wdrożenie odbędzie się w tym samym klastrze, w którym działa gitlab-runner. Musimy to zrobić, ponieważ w przeciwnym razie qbec będzie próbować znaleźć odpowiedni serwer Kubernetes w Twoim kubeconfig.
  • —wait — zmusza qbec do poczekania, aż tworzony przez niego zasoby przejdą w stan Gotowy, a dopiero potem zakończy się z udanym kodem wyjścia.
  • —yes — po prostu wyłącza interaktywny shell Czy na pewno? podczas wdrożenia.

Nie zapominajmy o zatwierdzeniu naszych zmian:

git add .gitlab-ci.yml
git commit -m "Zautomatyzuj wdrożenie"

A po tym git push zobaczymy, jak nasze aplikacje zostały wdrożone:

Zrzut ekranu drugiego pipeline'u

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

11. Artefakty i budowa przy push w masterze

Zazwyczaj powyższe kroki są wystarczające do budowy i dostarczenia prawie każdego mikroserwisu, jednak nie chcemy tagować za każdym razem, gdy potrzebujemy zaktualizować stronę. Dlatego wybierzemy bardziej dynamiczną drogę i skonfigurujemy wdrożenie na podstawie digest w gałęzi master.

Pomysł jest prosty: teraz obraz naszego website będzie budowany na nowo za każdym razem, gdy nastąpi push w master, a następnie automatycznie wdrożony w Kubernetes.

Zaktualizujmy te dwie zadania w naszym .gitlab-ci.yml:

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - mkdir -p $CI_PROJECT_DIR/artifacts
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
  artifacts:
    paths:
      - artifacts/
  only:
    refs:
      - master
      - tags

deploy_website:
  extends: .deploy_qbec_app
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

Zauważ, że dodaliśmy gałąź master do refs dla zadania build_website a teraz używamy $CI_COMMIT_REF_NAME zamiast $CI_COMMIT_TAG, to znaczy, że odpinamy się od tagów w Git i teraz będziemy pushować obraz z nazwą gałęzi commita, który zainicjował pipeline. Warto zauważyć, że to również będzie działać z tagami, co pozwoli nam zachować migawki strony z określoną wersją w docker-registry.

Kiedy nazwa tagu docker dla nowej wersji strony może pozostać niezmienna, nadal musimy opisać zmiany dla Kubernetes, w przeciwnym razie po prostu nie wdroży aplikacji z nowego obrazu, ponieważ nie zauważy żadnych zmian w manifeście wdrożenia.

Opcja —vm:ext-str digest="$DIGEST" dla qbec — pozwala przekazać zewnętrzną zmienną do jsonnet. Chcemy, aby z każdą wersją naszego aplikacja była ponownie wdrażana w klastrze. Nie możemy już używać nazwy tagu, która teraz może być niezmienna, ponieważ musimy wiązać się z konkretną wersją obrazu i uruchamiać wdrożenie przy jej zmianie.

W tym pomoże nam możliwość Kaniko zapisywania digestu obrazu do pliku (opcja —digest-file)
Następnie przekażemy ten plik i odczytamy go w momencie wdrożenia.

Zaktualizujemy parametry dla naszego deploy/website/environments/base.libsonnet który teraz będzie wyglądał tak:

{
  components: {
    website: {
      name: 'example-docs',
      image: 'registry.gitlab.com/kvaps/docs.example.org/website@' + std.extVar('digest'),
      replicas: 1,
      containerPort: 80,
      servicePort: 80,
      nodeSelector: {},
      tolerations: [],
      ingressClass: 'nginx',
      domain: 'docs.example.org',
    },
  },
}

Gotowe, teraz każdy commit w master inicjuje budowę obrazu dockerowego dla website, a następnie jego wdrożenie w Kubernetes.

Nie zapominajmy o zatwierdzeniu naszych zmian:

git add .
git commit -m "Konfiguracja dynamicznej budowy"

Sprawdźmy, po git push powinniśmy zobaczyć coś takiego:

Zrzut ekranu pipeline'a dla master

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Generalnie nie ma potrzeby ponownego wdrażania gitlab-runner przy każdym pushu, o ile oczywiście nic nie zmieniło się w jego konfiguracji, poprawmy to w .gitlab-ci.yml:

deploy_gitlab_runner:
  extends: .deploy_qbec_app
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  before_script:
    - base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
  script:
    - qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --wait --yes
  only:
    changes:
      - deploy/gitlab-runner/**/*

changes pozwoli to na śledzenie zmian w deploy/gitlab-runner/ i uruchomi naszą pracę tylko w przypadku ich wystąpienia

Nie zapominajmy o zatwierdzeniu naszych zmian:

git add .gitlab-ci.yml
git commit -m "Zmniejsz wdrożenie gitlab-runner"

git push, tak jest lepiej:

Zrzut ekranu zaktualizowanego pipeline'a

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

12. Dynamiczne środowiska

Nadszedł czas, aby urozmaicić nasz pipeline dynamicznymi środowiskami.

Na początek zaktualizujemy pracę build_website w naszym .gitlab-ci.yml, usuwając z niej blok only, co spowoduje, że Gitlab uruchomi ją przy każdym commicie w każdej gałęzi:

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - mkdir -p $CI_PROJECT_DIR/artifacts
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
  artifacts:
    paths:
      - artifacts/

Następnie zaktualizujemy pracę deploy_website, dodajmy tam blok environment:

deploy_website:
  extends: .deploy_qbec_app
  environment:
    name: prod
    url: https://docs.example.org
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

To pozwoli Gitlab powiązać zadanie z prod środowiskiem i wyświetlić odpowiedni link do niego.

Teraz dodajmy jeszcze dwa zadania:

deploy_website:
  extends: .deploy_qbec_app
  environment:
    name: prod
    url: https://docs.example.org
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

deploy_review:
  extends: .deploy_qbec_app
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: http://$CI_ENVIRONMENT_SLUG.docs.example.org
    on_stop: stop_review
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply review --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
  only:
    refs:
    - branches
  except:
    refs:
      - master

stop_review:
  extends: .deploy_qbec_app
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop
  stage: deploy
  before_script:
    - git clone "$CI_REPOSITORY_URL" master
    - cd master
  script:
    - qbec delete review --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
  variables:
    GIT_STRATEGY: none
  only:
    refs:
    - branches
  except:
    refs:
      - master
  when: manual

Będą one uruchamiane przy push do dowolnych gałęzi, z wyjątkiem master, i będą wdrażać wersję podglądową strony.

Widzimy nową opcję dla qbec: —app-tag — pozwala na etykietowanie wdrożonych wersji aplikacji i działa tylko w obrębie tej etykiety. Przy tworzeniu i usuwaniu zasobów w Kubernetes qbec będzie operować tylko na nich.
Dzięki temu możemy nie tworzyć oddzielnego środowiska dla każdej recenzji, a po prostu ponownie wykorzystać to samo.

Tutaj również używamy qbec apply review, zamiast qbec apply default — to właśnie w tym momencie postaramy się opisać różnice w naszych środowiskach (review i default):

Dodajmy środowisko review w deploy/website/qbec.yaml spec: environments: review: defaultNamespace: docs server: https://kubernetes.example.org:8443

Następnie ogłosimy je w

deploy/website/params.libsonnet local env = std.extVar('qbec.io/env'); local paramsMap = { _: import './environments/base.libsonnet', default: import './environments/default.libsonnet', review: import './environments/review.libsonnet', };if std.objectHas(paramsMap, env) then paramsMap[env] else error 'environment ' + env + ' not defined in ' + std.thisFile:

I zapiszmy niestandardowe parametry dla niego w

deploy/website/environments/review.libsonnet Przyjrzyjmy się uważniej zadaniu:

// this file has the param overrides for the default environment
local base = import './base.libsonnet';
local slug = std.extVar('qbec.io/tag');
local subdomain = std.extVar('subdomain');

base {
  components+: {
    website+: {
      name: 'example-docs-' + slug,
      domain: subdomain + '.docs.example.org',
    },
  },
}

stop_review , będzie ono wywoływane przy usuwaniu gałęzi i aby gitlab nie próbował zrobić checkout na nią, używamyGIT_STRATEGY: none , później klonujemy-gałąź i usuwamy recenzję przez nią. master-przejść do wątku i usunąć recenzję przez niego.
Trochę skomplikowane, ale nie znalazłem jeszcze piękniejszego sposobu.
Alternatywną opcją może być wdrożenie każdej recenzji w oddzielnej przestrzeni nazw, którą zawsze można całkowicie usunąć.

Nie zapominajmy o zatwierdzeniu naszych zmian:

git add .
git commit -m "Włącz automatyczną recenzję"

git push, git checkout -b test, git push origin test, sprawdzamy:

Zrzut ekranu utworzonych środowisk w Gitlabie

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Czy wszystko działa? — świetnie, usuwamy naszą testową gałąź: git checkout master, git push origin :test, sprawdzamy, czy zadania usuwające środowisko zakończyły się bez błędów.

Tutaj od razu chciałbym wyjaśnić, że tworzenie gałęzi może być w projekcie dowolnym deweloperem, może on także zmienić .gitlab-ci.yml plik i uzyskać dostęp do tajnych zmiennych.
Dlatego zdecydowanie zaleca się zezwolenie na ich użycie tylko dla chronionych gałęzi, na przykład w master, lub stworzenie oddzielnego zestawu zmiennych dla każdego środowiska.

13. Aplikacje Recenzji

Aplikacje przeglądowe to taka funkcjonalność GitLaba, która pozwala dodać przycisk do szybkiego podglądu każdego pliku w repozytorium w wdrożonym środowisku.

Aby te przyciski się pojawiły, należy stworzyć plik .gitlab/route-map.yml i opisać w nim wszystkie transformacje ścieżek, w naszym przypadku będzie to bardzo proste:

# Indices
- source: /content/(.+?)_index.(md|html)/ 
  public: '1'

# Pages
- source: /content/(.+?).(md|html)/ 
  public: '1/'

Nie zapominajmy o zatwierdzeniu naszych zmian:

git add .gitlab/
git commit -m "Włącz aplikacje recenzji"

git push, i sprawdzamy:

Zrzut ekranu przycisku Aplikacji Recenzji

Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Zadanie wykonane!

Źródła projektu:

Dziękuję za uwagę, mam nadzieję, że się podobało Testujemy nowe narzędzia do budowania i automatyzacji wdrożeń w Kubernetes

Ź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