
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 , która generowana jest z 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
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.orgI od razu git-repozytorium:
cd docs.example.org
git initNa 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 , 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/learnW 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 serverA pod adresem 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

Aby wygenerować stronę, wystarczy uruchomić:
hugoZawartość katalogu public/ będzie twoją stroną.
Tak, swoją drogą, dodajmy ją od razu do .gitignore:
echo /public > .gitignoreNie 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ę 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ć , 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.1Gdzie 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

4. Wprowadzenie do 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 deployZainicjujmy naszą pierwszą aplikację:
qbec init website
cd websiteObecnie struktura naszej aplikacji wygląda następująco:
.
├── components
├── environments
│ ├── base.libsonnet
│ └── default.libsonnet
├── params.libsonnet
└── qbec.yamlprzyjrzyjmy 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 defaultNa wyjściu zobaczycie wyrenderowane manifesty yaml, które zostaną zastosowane w klastrze default.
Świetnie, teraz zastosujemy:
qbec apply defaultNa 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 defaultaby 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-runnerGdzie:
- — 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

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-runnerTym 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-runnerTeraz 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.
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 defaultjeśli wszystko jest w porządku, możemy usunąć nasze wcześniej wdrożone wydanie przez Helm:
helm uninstall gitlab-runneri wdrożyć je ponownie, ale już przez qbec:
qbec apply default7. Wprowadzenie do 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 initJeśli masz klucz PGP, możesz od razu dodać się jako współpracownika do tego projektu:
git-crypt add-gpg-user kvapss@gmail.comDzię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/keyfileDzię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 !diffJak 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 -eW 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 locki w tym momencie wszystkie zaszyfrowane pliki zamienią się w binarne coś, czego nie będzie można odczytać.
Aby odszyfrować repozytorium, wykonać:
git crypt unlock8. 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/helmJak 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-runneri 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 defaulti zastosujmy nasze zmiany w Kubernetes:
qbec apply defaultNie 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:
- tagsZauważ, ż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.1Bę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

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; echouzyskaną 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

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 --yesWprowadziliś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

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

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

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: manualBę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 wdeploy/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 wdeploy/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

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
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

Zadanie wykonane!
Źródła projektu:
- na Gitlabie:
- na GitHubie:
Dziękuję za uwagę, mam nadzieję, że się podobało ![]()
Źródło: habr.com
