Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

Përshëndetje! Kohët e fundit kanë dalë shumë mjete të shkëlqyera për automatizimin e ndërtimit të imazheve Docker dhe për shpërndarjen në Kubernetes. Prandaj, vendosa të eksperimentoja me GitLab-in, të studioja mundësitë e tij, dhe natyrisht, të konfiguroja pipeline-in.

Inspirimi për këtë punë erdhi nga website kubernetes.io, i cili krijohet nga kodet burimore automatike, dhe për çdo tërheqje të dërguar, roboti krijon automatikisht një version preview të website-it me ndryshimet tuaja dhe ofron një lidhje për ta parë.

Unë kam përpjekur të ndërtoj një proces të ngjashëm nga e para, por tërësisht të ndërtuar mbi GitLab CI dhe mjete të lira, të cilat kam mësuar t'i përdor për shpërndarjen e aplikacioneve në Kubernetes. Sot, përfundimisht, do t'ju tregoj më shumë për to.

Në këtë artikull do të shqyrtohen mjete të tilla si:
Hugo, qbec, kaniko, git-crypt dhe GitLab CI në krijimin e mjediseve dinamike.

Përmbajtja

  1. Njoftimi me Hugo
  2. Përgatitja e Dockerfile
  3. Njoftimi me kaniko
  4. Njoftimi me qbec
  5. Provojmë GitLab-runner me Kubernetes-executor
  6. Shpërndarja e Helm-chart-eve me qbec
  7. Njoftimi me git-crypt
  8. Krijojmë imazhin e toolbox-it
  9. Pipeline dhe ndërtimi ynë i parë i imazheve sipas etiketave
  10. Automatizimi i deploy-it
  11. Artefaktet dhe ndërtimi me push në master
  12. Mjediset dinamike
  13. Review Apps

1. Njoftim me Hugo

Si një shembull i projektit tonë, do të provojmë të krijojmë një faqe për publikimin e dokumentacionit, e ndërtuar mbi Hugo. Hugo është një gjenerues statik për përmbajtjen.

Për ata që nuk janë të njohur me gjeneratorët statik, do të flas pak më shumë rreth tyre. Në dallim nga motorët e zakonshëm të faqeve me bazë të dhënash dhe ndonjë php, të cilat, në kërkesën e përdoruesit, gjenerojnë faqet në fluks, gjeneratorët statik janë ndërtuar pak ndryshe. Ata lejojnë marrjen e burimeve, për zakonisht është një grup skedarësh në formatin Markdown dhe shabllone, dhe më pas t'i kompiliojnë në një faqe të gatshme.

Pra, në dalje do të merrni një strukturë direktorish dhe një grup skedarësh html të gjeneruar, të cilat mund të ngarkoni thjesht në çdo host të lirë dhe të merrni një faqe funksionale.

Hugo mund të instalohet lokal dhe ta provosh në veprim:

Inicioni një faqe të re:

hugo new site docs.example.org

Dhe njëkohësisht git-repozitorin:

cd docs.example.org
git init

Faqja jonë është akoma e pastër dhe për të shtuar diçka, së pari na nevojitet të lidhemi me një temë. Tema është thjesht një grup templates dhe rregullash sipas të cilave gjenerohet faqja jonë.

Si temë do të përdorim Learn, që mendoj se përshtatet më së miri për një faqe dokumentacioni.

Një vëmendje të veçantë duhet t'i kushtohet faktit se nuk na nevojitet të ruajmë skedarët e temës në depozitat tona të projektit; përkundrazi, ne mund ta lidhim atë duke përdorur git submodule:

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

Në këtë mënyrë, depozita jonë do të përmbajë vetëm skedarët që i përkasin drejtpërdrejt projektit tonë, dhe tema e lidhur do të mbetet si një lidhje në një depozitë specifike dhe commit në të, që do të thotë se gjithmonë mund ta tërheqim nga burimi origjinal dhe nuk duhet të frikësohemi nga ndryshimet e papajtueshme.

Le të rregullojmë konfigurimin config.toml:

baseURL = "http://docs.example.org/"
languageCode = "en-us"
title = "My Docs Site"
theme = "learn"

Që në këtë fazë mund të startojmë:

hugo server

Dhe në adresën http://localhost:1313/ kontrolloni faqen tonë të sapokrijuar, të gjitha ndryshimet e bëra në direktorium automatikisht përditësojnë dhe faqja e hapur në shfletues është shumë e përshtatshme!

Le të përpiqemi të krijojmë një faqe kryesore në content/_index.md:

# My docs site

## Welcome to the docs!

You will be very smart :-)

Pamja e ekranit e faqes së sapokrijuar

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

Për të gjeneruar faqen, mjafton të startoni:

hugo

Përmbajtja e direktoriumit publik/ do të jetë faqja juaj.
Po, për të thënë, le ta fusim menjëherë në .gitignore:

echo /public > .gitignore

Mos harroni të komitoni ndryshimet tona:

git add .
git commit -m "New site created"

2. Përgatitja e Dockerfile

Ka ardhur koha të përcaktojmë strukturën e depozita tonë. Zakonisht përdor diçka të tillë:

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

  • dockerfiles/ — pĂ«rmbajnĂ« direktoriet me Dockerfile dhe gjithçka tĂ« nevojshme pĂ«r ndĂ«rtimin e imazheve tona docker.
  • deploy/ — pĂ«rmban direktoriet pĂ«r zhvillimin e aplikacioneve tona nĂ« Kubernetes

Kështu që Dockerfile ynë i parë do ta krijojmë në rrugën 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" ]

Si mund ta vini re, Dockerfile përmban dy FROM, kjo mundësi quhet ndërtimi multi-stage dhe lejon të përjashtojë nga imazhi përfundimtar docker çdo gjë të panevojshme.
KĂ«shtu imazhi pĂ«rfundimtar do tĂ« pĂ«rmbajĂ« vetĂ«m darkhttpd (njĂ« server HTTP tĂ« lehtĂ«) dhe publik/ — pĂ«rmbajtjen e faqes sonĂ« tĂ« gjeneruar statikisht.

Mos harroni të komitoni ndryshimet tona:

git add dockerfiles/website
git commit -m "Shto Dockerfile për faqen"

3. Njohuni me kaniko

Si ndërtues i imazheve docker, vendosa të përdor kaniko, pasi për funksionimin e tij nuk kërkohet një demon docker, dhe ndërtimi mund të bëhet në çdo makinë dhe të ruhet cache direkt në registri, duke eliminuar kështu nevojën për një ruajtje të plotë.

Për ndërtimin e imazhit mjafton të lançoni një kontenier me kaniko executor dhe t'i transmetoni atij kontekstin aktual të ndërtimit, diçka që mund ta bëni dhe lokalisht, përmes 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

Ku registry.gitlab.com/kvaps/docs.example.org/website — emri i imazhit tuaj docker, pas ndĂ«rtimit do tĂ« dĂ«rgohet automatikisht nĂ« registrin docker.

Parametri —cache lejon tĂ« ruajĂ« shtresat nĂ« docker registry, pĂ«r shembullin e dhĂ«nĂ« ato do tĂ« ruhen nĂ« registry.gitlab.com/kvaps/docs.example.org/website/cache, por mund tĂ« tregoni njĂ« rrugĂ« tjetĂ«r duke pĂ«rdorur parametrin --cache-repo.

Kapja e ekranit docker-registry

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

4. Njohja me qbec

Qbec është një mjet deployimi që lejon të përshkruani deklarativisht manifestet e aplikacionit tuaj dhe t'i deployoni ato në Kubernetes. Përdorimi i Jsonnet si sintaksë kryesore e thjeshton shumë përshkrimin e diferencave për mjedise të shumta, si dhe shp almost e eliminon përsëritjen e kodit.

Kjo mund të jetë veçanërisht relevante në rastet kur ju nevojitet të deployoni një aplikacion në disa klasterë me parametra të ndryshëm dhe dëshironi të përshkruani ata deklarativisht në Git.

Qbec gjithashtu lejon të renderoni Helm chart duke u kaluar atyre parametrat e nevojshëm dhe më pas të punoni me to ashtu si me manifestet e zakonshme, duke përfshirë duke u vendosur mbi ta ndryshime të ndryshme, dhe kjo, nga ana e saj, lejon që të eliminohet nevoja për të përdorur ChartMuseum. Do të thotë, mund të ruani dhe renderoni chartet direkt nga git, ku mjekët e tyre janë.

Siç thashë më parë, të gjitha deploy-t do t'i ruajmë në direktorinë deploy/:

mkdir deploy
cd deploy

Le të inicializojmë aplikacionin tonë të parë:

qbec init website
cd website

Struktura e aplikacionit tonë tani duket kështu:

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

të shohim skedarin qbec.yaml:

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

Këtu na intereson kryesisht spec.environments, qbec tashmë ka krijuar për ne ambientin default dhe ka marrë adresën e serverit, si dhe namespace nga kubeconfig-i ynë aktual.
Tani kur bëhet deploy në default ambient, qbec gjithmonë do të bëjë deploy vetëm në Kubernetes-cluster-in e përcaktuar dhe në namespace-in e caktuar, pra nuk do t'ju duhet më të kaloni mes kontekstesh dhe namespace-esh për të realizuar deploy.
Në rast nevoje, gjithmonë mund të azhurnoni cilësimet në këtë skedar.

Të gjitha ambientet tuaja përshkruhen në qbec.yaml, dhe në skedarin params.libsonnet, ku thuhet se nga ku duhet të merrni parametrat për to.

Më tej shohim dy direktorë:

  • components/ — kĂ«tu do tĂ« ruhen tĂ« gjitha manifestet pĂ«r aplikacionin tonĂ«, ato mund tĂ« pĂ«rshkruhen si nĂ« jsonnet ashtu edhe nĂ« skedarĂ«t e zakonshĂ«m yaml
  • mjediset/ — kĂ«tu do tĂ« pĂ«rshkruajmĂ« tĂ« gjitha variablat (parametrat) pĂ«r mjediset tona.

Me përjashtim të rastit, ne kemi dy skedarë:

  • mjediset/base.libsonnet — ai do tĂ« pĂ«rmbajĂ« parametrat e pĂ«rbashkĂ«t pĂ«r tĂ« gjitha mjediset
  • mjediset/default.libsonnet — pĂ«rmban parametrat e tejkaluar pĂ«r mjedisin default

Le të hapim mjediset/base.libsonnet dhe të shtojmë parametrat për komponentin tonë të parë:

{
  komponentet: {
    website: {
      emri: 'example-docs',
      imazhi: 'registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1',
      replika: 1,
      portaEkonfiguratorit: 80,
      portaESherbimit: 80,
      përzgjedhësiINodit: {},
      tolerancat: [],
      klasaEPërshkimit: 'nginx',
      domeni: 'docs.example.org',
    },
  },
}

Të krijojmë gjithashtu komponentin tonë të parë komponentet/website.jsonnet:

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

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

Në këtë skedar, ne përshkruam tri entitete të Kubernetes, këto janë: Deployment, Shërbimi dhe Ingress. Nëse dëshironim, mund t'i ndanim ata në komponentë të ndryshëm, por në këtë fazë një është e mjaftueshme për ne.

Sintaksa jsonnet është shumë e ngjashme me json-in e zakonshëm, në të vërtetë json-i i zakonshëm është një jsonnet i vlefshëm, kështu që për një kohë të parë mund t'ju duket më e lehtë të përdorni shërbime online si yaml2json për të konvertuar yaml-in tuaj të zakonshëm në json, ose, nëse komponentët tuaj nuk përmbajnë variabla, atëherë ata mund të përshkruhen si yaml i zakonshëm.

Kur punoni me jsonnet unë ju rekomandoj shumë të instaloni një plugin për redaktorin tuaj

Për shembuj, për vim ka një plugin vim-jsonnet, i cili përfshin ndriçimin e sintaksës dhe ekzekuton automatikisht jsonnet fmt me çdo ruajtje (kërkon që jsonnet të jetë instaluar).

Të gjitha janë gati, tani mund të fillojmë të depelojmë:

Për ta parë se çfarë kemi arritur, ekzekutojmë:

qbec show default

Në përfundim do të shihni manifestet yaml të renderuara që do të aplikohen në klastrin default.

Shkëlqyer, tani aplikojmë:

qbec apply default

Në përfundim gjithmonë do të shihni se çfarë do të bëhet në klastrin tuaj, qbec do t'ju kërkojë të pranoni ndryshimet, duke shkruar y do t'alejoni qëllimet tuaja.

Tani aplikacioni ynë është i vendosur!

Në rast se bëni ndryshime, gjithmonë mund të ekzekutoni:

qbec diff default

për të parë se si këto ndryshime do të reflektohen në vendosjen aktuale

Mos harroni të komitoni ndryshimet tona:

cd ../..
git add deploy/website
git commit -m "Shtoni vendosjen për uebfaqen"

5. Po provojmë Gitlab-runner me Kubernetes-executor

Derisa kohët e fundit kam përdorur vetëm gitlab-runner në një makinë të përgatitur paraprakisht (në kontenier LXC) me executor shell ose docker. Fillimisht kishim disa nga këta ranerë të përcaktuar globalisht në Gitlab-in tonë. Ata grumbullonin imazhe docker për të gjithë projektet.

Por siç tregoi praktika — kjo zgjidhje nuk ishte mĂ« e mira, si pĂ«r nga prakticiteti ashtu edhe pĂ«r nga siguria. MĂ« mirĂ« dhe ideologjikisht ishte tĂ« kisha ranerĂ« tĂ« veçantĂ« tĂ« vendosur pĂ«r secilin projekt, ndoshta edhe pĂ«r secilĂ«n mjedis.

Me fat kjo nuk është aspak një problem, sepse tani do të vendosim gitlab-runner drejtpërdrejt si pjesë e projektit tonë në Kubernetes.

Gitlab ofron njĂ« helm-chart tĂ« gatshĂ«m pĂ«r vendosjen e gitlab-runner nĂ« Kubernetes. KĂ«shtu qĂ« gjithçka qĂ« ju nevojitet, Ă«shtĂ« tĂ« mĂ«soni registration token pĂ«r projektin tonĂ« nĂ« Settings —> CI / CD —> Runners dhe t'ja jepni 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

Ku:

  • https://gitlab.com — adresa e serverit tuaj Gitlab.
  • yga8y-jdCusVDn_t4Wxc — tokeni i regjistrimit pĂ«r projektin tuaj.
  • rbac.create=true — i jep running-it numrin e nevojshĂ«m tĂ« privilegjeve pĂ«r tĂ« krijuar podĂ« pĂ«r tĂ« bĂ«rĂ« detyrat tona me ndihmĂ«n e kubernetes-executor.

Nëse gjithçka është bërë siç duhet, duhet të shihni running-un e regjistruar në seksionin Runners, në cilësimet e projektit tuaj.

Screenshot i running-ut të shtuar

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

A Ă«shtĂ« kaq e thjeshtĂ«? — po, kaq e thjeshtĂ«! AsnjĂ« mĂ« shumĂ« mundimes me regjistrimin manual tĂ« running-Ă«ve, nga ky moment running-Ă«t do tĂ« krijohen dhe asgjĂ«sohen automatikisht.

6. Deploy Helm-charts me QBEC

Tani që kemi vendosur ta shqyrtojmë gitlab-runner si pjesë e projektit tonë, është koha të përshkruajmë atë në depo git.

Mund tĂ« pĂ«rshkruajmĂ« atĂ« si njĂ« komponent tĂ« veçantĂ« website, por nĂ« tĂ« ardhmen planifikojmĂ« tĂ« bĂ«jmĂ« deploy kopje tĂ« ndryshme website shumĂ« shpesh, ndryshe nga gitlab-runner, i cili do tĂ« deployohet vetĂ«m njĂ« herĂ« pĂ«r çdo Kubernetes-cluster. Pra, le tĂ«ćˆć§‹ćŒ–ojmĂ« njĂ« aplikacion tĂ« veçantĂ« pĂ«r tĂ«:

cd deploy
qbec init gitlab-runner
cd gitlab-runner

Këtë herë nuk do të përshkruajmë entitetet Kubernetes manualisht, por do të marrim një Helm-chart të gatshëm. Një nga përparësitë e qbec është mundësia për të renderuar Helm-chartet drejtpërdrejt nga repository Git.

Le të lidhim atë duke përdorur git submodule:

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

Tani direktoria vendor/gitlab-runner ka një repository me chartin për gitlab-runner.

Në mënyrë të ngjashme, mund të lidhni edhe repo të tjera, për shembull edhe një repo të plotë me chartet zyrtare. https://github.com/helm/charts

Le të përshkruajmë komponentin 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,
  }
)

Argumenti i parĂ« pĂ«r expandHelmTemplate ne e kalojmĂ« rrugĂ«n pĂ«r nĂ« chart, pastaj params.values, tĂ« cilat do t’i marrim nga parametrat e mjedisit, pastaj vjen njĂ« objekt me

  • nameTemplate — emri i versionit
  • namespace — namespace qĂ« i kalon Helm-it
  • thisFile — njĂ« parametĂ«r i detyrueshĂ«m, qĂ« kalon rrugĂ«n pĂ«r nĂ« skedarin aktual
  • verbose — tregon komandĂ«n helm template me tĂ« gjitha argumentet gjatĂ« renderimit tĂ« chartit

Tani do të përshkruajmë parametrat për komponentin tonë në mjediset/base.libsonnet:

sekretet lokale = import '../secrets/base.libsonnet';

{
  komponentet: {
    gitlabRunner: {
      emri: 'gitlab-runner',
      vlerat: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          krijo: true,
        },
        runnerRegistrationToken: sekretet.runnerRegistrationToken,
      },
    },
  },
}

Kërkojmë vëmendje runnerRegistrationToken ne marrim nga një skedar jasht secrets/base.libsonnet, le të krijojmë atë:

{
  runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}

Të shohim nëse gjithçka funksionon:

qbec show default

nëse gjithçka është në rregull, atëherë mund të fshini versionin tonë të mëparshëm të lançuar përmes Helm:

helm uninstall gitlab-runner

dhe ta lançojmë atë përsëri, por tashmë përmes qbec:

qbec apply default

7. Njoftimi me git-crypt

Git-crypt — Ă«shtĂ« njĂ« mjet qĂ« lejon tĂ« konfigurohet enkriptimi transparent pĂ«r repozitorin tuaj.

Aktualisht, struktura e drejtorisë tonë për gitlab-runner duket kështu:

.
├── komponentet
│   ├── gitlab-runner.jsonnet
├── ambientet
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
├── qbec.yaml
├── sekretet
│   └── base.libsonnet
└── vendor
    └── gitlab-runner (submodul)

Por ruajtja e sekretëve në Git është e pasigurt, apo jo? Prandaj na nevojitet t'i enkriptojmë ato në mënyrë të duhur.

Zakonisht për një variabël të vetëm, kjo nuk ka gjithmonë kuptim. Ju mund të kaloni sekretet në qbec dhe përmes variablave të mjedisit të CI-sistemit tuaj.
Megjithatë, është e rëndësishme të theksohet se ka projekte më të komplikuara, të cilat mund të përmbajnë shumë më tepër sekrete, dhe ndarja e të gjitha këtyre përmes variablave të mjedisit do të ishte shumë e vështirë.

Për më tepër, në këtë rast nuk do të mund t'ju tregoja për një mjet kaq të shkëlqyer si git-crypt.

git-crypt po ashtu është i dobishëm sepse ruan të gjithë historinë e sekreteve, si dhe lejon krahasimin, bashkimin dhe zgjidhjen e konflikteve ashtu siç jemi mësuar ta bëjmë me Git.

Gjëja e parë që duhet të bëjmë pas instalimit git-crypt është të gjenerojmë çelësat për depozitat tona:

git crypt init

Nëse keni një çelës PGP, mund ta shtoni menjëherë veten si bashkëpunëtor për këtë projekt:

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

Kështu, gjithmonë do të jeni në gjendje të dekriptoni këtë depo me çelësin tuaj privat.

Nëse nuk keni një çelës PGP dhe nuk planifikoni të keni, mund të zgjidhni një rrugë tjetër dhe të eksportoni çelësin e projektit:

git crypt export-key /path/to/keyfile

Kështu, kushdo që ka çelësin e eksportuar keyfile do të mund të dekriptojë depozitën tuaj.

Ka ardhur koha për të konfigurimin e sekreteve tona të para.
Të rikujtojmë, ne ende jemi në direktorinë deploy/gitlab-runner/, ku e kemi një direktori secrets/, le të enkriptojmë të gjitha skedarët në të, për këtë do të krijojmë skedarin secrets/.gitattributes me këtë përmbajtje:

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

Siç shihet nga përmbajtja, të gjitha skedarët sipas maskës * do të kalojnë përmes git-crypt, përveç vetë .gitattributes

Këtë mund ta kontrollojmë duke ekzekutuar:

git crypt status -e

Në dalje do të marrim një listë të të gjitha skedarëve në depo për të cilët është aktivizuar enkriptimi

Ja ku jemi, tani mund të angazhojmë ndryshimet tona me besim:

cd ../..
git add .
git commit -m "Shto deploy për gitlab-runner"

Për të bllokuar depozitën mjafton të ekzekutoni:

git crypt lock

dhe menjëherë të gjitha skedarët e enkriptuar do të shndërrohen në diçka binarike, do të jetë e pamundur t'i lexoni ato.
Për të dekriptuar depozitën, ekzekutoni:

git crypt unlock

8. Krijoni imazhin e toolbox

Imazhi i toolbox është një imazh me të gjitha mjetet që do të përdorim për implikimin e projektit tonë. Ai do të përdoret nga gitlab-runner për të kryer detyra tipike të implikimit.

Këtu është e thjeshtë, krijoni një të ri dockerfiles/toolbox/Dockerfile me këtë përmbajtje:

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

Si si duket, në këtë imazh ne instaluam të gjitha utilitetet që përdorëm për të dëbuar aplikacionin tonë. Këtu nuk kemi nevojë për kubectl, por ndoshta do të dëshironit të luanit me të në fazën e konfigurimit të pipeline-it.

Po ashtu, për të qenë në gjendje të komunikojmë me Kubernetes dhe të kryejmë deploje në të, na nevojitet të konfigurojmë një rol për pod-ët e gjeneruar nga gitlab-runner.

Për këtë, do të kalojmë në direktorinë me gitlab-runner:

cd deploy/gitlab-runner

dhe do të shtojmë një komponent të ri 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,
      },
    ],
  },
]

Po ashtu do të përshkruajmë parametrat e rinj në mjediset/base.libsonnet, i cili tani duket kështu:

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',
    },
  },
}

Kërkojmë vëmendje $.components.rbac.name i referohet emri për komponentin rbac

Le të verifikojmë se çfarë ka ndryshuar:

qbec diff default

dhe të aplikojmë ndryshimet tona në Kubernetes:

qbec apply default

Mos harrojmë të angazhojmë ndryshimet tona në git:

cd ../..
git add dockerfiles/toolbox
git commit -m "Shto Dockerfile për toolbox"
git add deploy/gitlab-runner
git commit -m "Konfiguro gitlab-runner për të përdorur toolbox"

9. Të parin tonë pipeline dhe ndërtimi i imazheve sipas etiketimeve

Në rrënjën e projektit do të krijojmë .gitlab-ci.yml me këtë përmbajtje:

.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

Kujdes, ne përdorim GIT_SUBMODULE_STRATEGY: normal për ato detyra ku duhet të inicializojmë në mënyrë të qartë submodulin para ekzekutimit.

Mos harroni të komitoni ndryshimet tona:

git add .gitlab-ci.yml
git commit -m "Automatizo ndërtimin e docker"

Mendoj se mund ta quajmë këtë version v0.0.1 dhe të vendosim një etiketë:

git tag v0.0.1

Ne do t'i vendosim etiketat çdo herĂ« qĂ« na nevojitet tĂ« lĂ«shojmĂ« njĂ« version tĂ« ri. Etiketat nĂ« imazhet Docker do tĂ« lidhen me etiketat Git. Çdo push me njĂ« etiketĂ« tĂ« re do tĂ« inicojĂ« ndĂ«rtimin e imazheve me kĂ«tĂ« etiketĂ«.

Do ta kryejmë git push --tags, dhe do të shikojmë në pipeline tonë të parë:

Screenshot i pipeline-t të parë

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

Vlen të theksohet se ndërtimi për etiketa është i përshtatshëm për ndërtimin e imazheve docker, por nuk është i përshtatshëm për depolimin e aplikacionit në Kubernetes. Duke qenë se etiketa të reja mund të caktohen edhe për komitë të vjetra, në këtë rast inicoimi i pipeline-t për to do të çojë në depolimin e versionit të vjetër.

Për të zgjidhur këtë problem, zakonisht ndërtimi i imazheve docker lidhet me etiketat, ndërsa depolimi i aplikacionit me degën master, në të cilën janë të ngurtësuara versionet e imazheve të ndërtuara. Vetëm në këtë rast mund të inicojme një kthim mbrapa me një revert master-dege.

10. Automatizimi i depolimit

Në mënyrë që Gitlab-runner të mund të deshifrojë sekretet tona, do të na nevojitet të eksportojmë çelësin e repository-t dhe ta shtojmë atë në variablat mjedisore të CI-së tonë:

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

stringun e marrë do ta ruajmë në Gitlab, për këtë do të kalojmë në konfigurimet e projektit tonë:
C ĐœĐ°ŃŃ‚Ń€ĐŸĐčĐșĐž —> CI / CD —> Variables

Dhe do të krijojmë një variabël të re:

Tipi
Key
Vlera
E mbrojtur
E maskuar
Shkalla

File
GITCRYPT_KEY
<stringu juaj>
e vërtetë (për kohën e trajnimit, mund edhe false)
e vërtetë
TĂ« gjitha ambientet

Screenshot i variablës së shtuar

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

Tani do ta azhurnojmë tonë .gitlab-ci.yml duke shtuar në të:

.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

Këtu ne kemi përdorur disa mundësi të reja për qbec:

  • —root some/app — lejon tĂ« pĂ«rcaktoni direktorine e aplikacionit specifik
  • —force:k8s-context __incluster__ — kjo Ă«shtĂ« njĂ« variabĂ«l magjike qĂ« tregon se deploy do tĂ« ndodhi nĂ« tĂ« njĂ«jtin klaster ku Ă«shtĂ« nisur gtilab-runner. Duhet ta bĂ«ni kĂ«tĂ«, pasi ndryshe qbec do tĂ« provojĂ« tĂ« gjejĂ« njĂ« server Kubernetes tĂ« pĂ«rshtatshĂ«m nĂ« kubeconfig tuaj
  • —wait — e bĂ«n qbec tĂ« presĂ« derisa burimet e krijuara tĂ« kalojnĂ« nĂ« gjendjen Ready dhe pastaj tĂ« pĂ«rfundojnĂ« me njĂ« exit-code tĂ« suksesshĂ«m.
  • —yes — thjesht çon nĂ« mbylljen e shell-it interaktiv A jeni tĂ« sigurt? nĂ« deploy.

Mos harroni të komitoni ndryshimet tona:

git add .gitlab-ci.yml
git commit -m "Automate deploy"

Dhe pas git push do të shohim si u kanë dëshkuar aplikacionet tona:

Screenshot i pipeline-it të dytë

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

11. Artefaktet dhe ndërtimi gjatë push në master

Në përgjithësi, hapat e mësipërm janë të mjaftueshëm për ndërtimin dhe shpërndarjen e pothuajse çdo mikroshërbimi, por ne nuk duam të vendosim një etiketë çdo herë kur na nevojitet të përditësojmë faqen. Prandaj, do të ndjekim një rrugë më dinamike dhe do të konfigurojmë shpërndarjen sipas digest në degën master.

Ideja është e thjeshtë: tani imazhi ynë website do të ricampohet çdo herë kur bëhet një push në master, dhe më pas automatikisht do të shpërndahen në Kubernetes.

Le të përditësojmë këto dy punë në .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"

Vini re, kemi shtuar degën master për refs për punën build_website dhe tani po përdorim $CI_COMMIT_REF_NAME në vend të $CI_COMMIT_TAG, pra ndaj ne ndalojmë lidhjen me etiketat në Git dhe tani do të dorëzojmë imazhin me emrin e degës së komitit që aktivizoi pipeline. Vlen të theksohet se kjo do të funksionojë gjithashtu me etiketat, duke na lejuar të ruajmë snapshotet e faqes me një version të caktuar në docker-registry.

Kur emri i etiketës docker për versionin e ri të faqes mund të mbetet i pandryshuar, ne ende duhet të përshkruajmë ndryshimet për Kubernetes, në të kundërt do të thotë se ai thjesht nuk do të ri-distribuojë aplikacionin nga imazhi i ri, pasi nuk do të vërejë asnjë ndryshim në manifestin e shpërndarjes.

Opcioni —vm:ext-str digest=»$DIGEST» pĂ«r qbec — lejon tĂ« kalosh njĂ« variabĂ«l tĂ« jashtme nĂ« jsonnet. Ne duam qĂ« me çdo lĂ«shim tĂ« aplikacionit tonĂ« tĂ« ri-distribuohet nĂ« klasĂ«r. Tani nuk mund tĂ« pĂ«rdorim emrin e etiketĂ«s qĂ« tani mund tĂ« mbetet i pandryshuar, pasi duhet tĂ« lidhim nĂ« njĂ« version tĂ« caktuar tĂ« imazhit dhe tĂ« aktivizojmĂ« shpĂ«rndarjen kur ai ndryshon.

KĂ«tu na ndihmon mundĂ«sia e Kaniko pĂ«r tĂ« ruajtur digestin e imazhit nĂ« njĂ« skedar (opsioni —digest-file)
Pastaj këtë skedar do ta kalojmë dhe e lexojmë në momentin e shpërndarjes.

Do të përmirësojmë parametrat për deploy/website/environments/base.libsonnet i cili tani do të duket kështu:

{
  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',
    },
  },
}

I gatshëm, tani çdo komitim në master do të inicializojë ndërtimin e imazhit docker për website, dhe pastaj do ta vendosë atë në Kubernetes.

Mos harroni të komitoni ndryshimet tona:

git add .
git commit -m "Configure dynamic build"

Le të kontrollojmë, pas git push duhet të shohim diçka të tillë:

Screenshot i pipeline për master

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

Nësë e kemi të panevojshme të ri-vendosim gitlab-runner pas çdo dorëzimi, nëse, sigurisht, asgjë nuk është ndryshuar në konfigurimin e tij, le të e korrigjojmë këtë në .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/**/*

ndryshime do të lejojë të monitorojmë ndryshimet në deploy/gitlab-runner/ dhe do të aktivizojë punën tonë vetëm në rast të tillë

Mos harroni të komitoni ndryshimet tona:

git add .gitlab-ci.yml
git commit -m "Reduce gitlab-runner deploy"

git push, kështu është më mirë:

Screenshot i pipeline-it të përditësuar

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

12. Ambientet dinamike

Ka ardhur koha të diversifikojmë pipeline-in tonë me ambiente dinamike.

Së pari, le të përditësojmë punën build_website në tonin tonë .gitlab-ci.yml, duke e hequr bllokun vetëm, që do të detyrojë Gitlab të aktivizojë atë me çdo commit në çdo degë:

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/

Pastaj do të përditësojmë punën deploy_website, do të shtojmë një bllok 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"

Kjo do të lejojë Gitlab të asociojë punën me prod ambientin dhe të shfaqë lidhjen e duhur për të.

Tani do të shtojmë edhe dy punë:

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

Do të aktivizohen me push në çdo degë përveç master dhe do të lançojnë versionin paraprak të faqes.

Shohim njĂ« opsion tĂ« ri pĂ«r qbec: —app-tag — lejon tĂ« etiketosh versionet e lançuara tĂ« aplikacionit dhe tĂ« punosh vetĂ«m brenda kĂ«saj etikete; nĂ« krijimin dhe shkatĂ«rrimin e burimeve nĂ« Kubernetes, qbec do tĂ« operojĂ« vetĂ«m me to.
Kështu, ne mund të mos krijojmë një ambient të veçantë për çdo review, por thjesht të ripërdorim të njëjtin.

KĂ«tu po ashtu pĂ«rdorim qbec apply review, nĂ« vend tĂ« qbec apply default — kjo Ă«shtĂ« pikĂ«risht ajo moment kur do tĂ« pĂ«rpiqemi tĂ« pĂ«rshkruajmĂ« dallimet pĂ«r ambientet tona (review dhe default):

Do të shtojmë review ambientin në deploy/website/qbec.yaml

spec:
  ambientet:
    review:
      defaultNamespace: docs
      server: https://kubernetes.example.org:8443

Pastaj do ta shpallim atë në 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 'ambienti ' + env + ' nuk është i definuar në ' + std.thisFile

Dhe do të shkruajmë parametra personalizuar për të në deploy/website/environments/review.libsonnet:

// 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',
    },
  },
}

Le të shohim gjithashtu më me kujdes punën stop_review, ajo do të aktivizohet kur të fshihet dega dhe që gitlab të mos përpiqet të bëjë checkout në të, përdoret GIT_STRATEGY: none, më vonë do të klonojmë master-degën dhe do të fshijmë review përmes saj.
Pak e ndërlikuar, por nuk kam gjetur një mënyrë më të bukur deri tani.
Një alternativë mund të jetë deployment-i i çdo review në një hapësirë të veçantë, e cila gjithmonë mund të hiqet komplet.

Mos harroni të komitoni ndryshimet tona:

git add .
git commit -m "Aktivizo automatic review"

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

Screenshoti i ambienteve të krijuara në Gitlab

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

A gjithçka funksionon? — shkĂ«lqyeshĂ«m, le tĂ« fshijmĂ« degĂ«n tonĂ« testuese: git checkout master, git push origin :test, verifikohet qĂ« punĂ«t pĂ«r fshirjen e ambientit kanĂ« pĂ«rfunduar pa gabime.

Këtu është mirë të sqarojmë se çdo zhvillues në projekt mund të krijojë degë, gjithashtu ai mund të modifikojë .gitlab-ci.yml skedarin dhe të ketë akses në variablat sekrete.
Prandaj, rekomandohet fuqishëm të lejohet përdorimi i tyre vetëm për degët e mbrojtura, për shembull në master, ose të krijohet një grup i veçantë variablash për çdo ambient.

13. Review Apps

Review Apps këto janë mundësitë e Gitlab që lejojnë çdo skedë në depo të ketë një buton për shikimin e shpejtë në ambientin e deploy-uar.

Për t'i pasur këto butona, duhet të krijoni skedarin .gitlab/route-map.yml dhe të përshkruani aty të gjitha transformimet e rrugëve, në rastin tonë kjo do të jetë shumë e thjeshtë:

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

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

Mos harroni të komitoni ndryshimet tona:

git add .gitlab/
git commit -m "Aktivizo aplikacionet e rishikimit"

git push, dhe verifikohet:

Screenshoti i butonit Review App

Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

Puna u krye!

Burimet e projektit:

Faleminderit për vëmendjen, shpresoj se ju pëlqeu Po provozim mjete të reja për ndërtimin dhe automatizimin e shpërndarjes në Kubernetes

Burimi: habr.com

Bli njĂ« hosting tĂ« besueshĂ«m pĂ«r faqet me mbrojtje DDoS, VPS VDS serverĂ« đŸ”„ Bli njĂ« hosting tĂ« besueshĂ«m pĂ«r faqet me mbrojtje DDoS, VPS VDS serverĂ« | ProHoster