Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Ciao! Negli ultimi tempi sono stati rilasciati molti strumenti interessanti per l'automazione sia della creazione di immagini Docker che del deploy in Kubernetes. Per questo ho deciso di sperimentare con GitLab, approfondire le sue funzionalità e, naturalmente, configurare un pipeline.

L'ispirazione per questo lavoro è stato il sito kubernetes.io, generato a partire da codici sorgente in modo automatico, e per ogni richiesta di pull inviata, un robot genera automaticamente una versione di anteprima del sito con le vostre modifiche e fornisce un link per la visualizzazione.

Ho cercato di costruire un processo simile da zero, completamente basato su GitLab CI e strumenti open source che utilizzo per il deploy delle applicazioni in Kubernetes. Oggi, finalmente, vi parlerò di essi in modo più dettagliato.

In questo articolo tratteremo strumenti come:
Hugo, qbec, kaniko, git-crypt e GitLab CI per la creazione di ambienti dinamici.

Indice

  1. Introduzione a Hugo
  2. Preparazione del Dockerfile
  3. Introduzione a kaniko
  4. Introduzione a qbec
  5. Proviamo GitLab Runner con l'executor Kubernetes
  6. Deploy di Helm Chart con qbec
  7. Introduzione a git-crypt
  8. Creiamo un'immagine toolbox
  9. Il nostro primo pipeline e la creazione di immagini per tag
  10. Automazione del deploy
  11. Artifact e build durante il push su master
  12. Ambienti dinamici
  13. App di revisione

1. Introduzione a Hugo

Come esempio del nostro progetto, tenteremo di creare un sito per la pubblicazione di documentazione costruito su Hugo. Hugo è un generatore di contenuti statico.

Per chi non è familiare con i generatori statici, parlerò brevemente di loro. A differenza dei normali motori di sito con database e qualche tipo di php, che generano pagine on-the-fly su richiesta dell'utente, i generatori statici funzionano in modo leggermente diverso. Permettono di prendere i file sorgente, che di solito sono una serie di file in markup Markdown e modelli di temi, e poi compilarli in un sito completamente pronto.

Quindi, come risultato, otterrete una struttura di directory e un insieme di file html generati, che potranno essere semplicemente caricati su qualsiasi hosting economico per avere un sito funzionante.

Hugo può essere installato localmente e provato sul campo:

Inizializziamo un nuovo sito:

hugo new site docs.example.org

E insieme a un repository git:

cd docs.example.org
git init

Per il momento, il nostro sito è completamente vuoto e per far sì che qualcosa appaia, dobbiamo prima collegare un tema. Un tema non è altro che un insieme di template e regole definite secondo le quali viene generato il nostro sito.

Come tema, utilizzeremo Learn, che, a mio avviso, è perfettamente adatta per un sito di documentazione.

È importante notare che non è necessario mantenere i file del tema nel repository del nostro progetto; invece, possiamo semplicemente collegarlo utilizzando git submodule:

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

In questo modo, nel nostro repository ci saranno solo i file direttamente correlati al nostro progetto, mentre il tema collegato rimarrà come un link a un repository specifico e al relativo commit, quindi potrà sempre essere estratto dalla fonte originale senza preoccuparsi di modifiche incompatibili.

Modifichiamo il file config.toml:

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

Già a questo punto, possiamo avviare:

hugo server

E all'indirizzo http://localhost:1313/ Controlla il nostro sito appena creato, tutte le modifiche effettuate nella directory vengono automaticamente aggiornate e visualizzate nella pagina aperta nel browser, molto comodo!

Proviamo a creare una pagina di titolo in content/_index.md:

# My docs site

## Welcome to the docs!

You will be very smart :-)

Screenshot della pagina appena creata

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Per generare il sito è sufficiente eseguire:

hugo

Il contenuto della directory public/ sarà il tuo sito.
Sì, a proposito, mettiamola subito in .gitignore:

echo /public > .gitignore

Non dimentichiamo di commettere le nostre modifiche:

git add .
git commit -m "Nuovo sito creato"

2. Preparazione del Dockerfile

È ora di definire la struttura del nostro repository. Di solito uso qualcosa del tipo:

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

  • dockerfiles/ — contengono directory con Dockerfiles e tutto il necessario per costruire le nostre immagini Docker.
  • deploy/ — contiene directory per il deploy delle nostre applicazioni in Kubernetes

In questo modo creeremo il nostro primo Dockerfile nel percorso 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" ]

Come potete notare, il Dockerfile contiene due DA, questa funzionalità è chiamata multi-stage build e consente di escludere tutto ciò che non è necessario dall'immagine finale del docker.
In questo modo, l'immagine finale conterrà solo darkhttpd (un server HTTP leggero) e public/ — il contenuto del nostro sito staticamente generato.

Non dimentichiamo di commettere le nostre modifiche:

git add dockerfiles/website
git commit -m "Aggiungi Dockerfile per il sito"

3. Introduzione a kaniko

Come costruttore di immagini docker, ho deciso di utilizzare kaniko, poiché non richiede la presenza del demone docker, e la build può essere eseguita su qualsiasi macchina, conservando la cache direttamente nel registry, eliminando così la necessità di disporre di un sistema di archiviazione persistente completo.

Per costruire l'immagine, basta avviare un contenitore con kaniko executor e passargli il contesto di costruzione corrente, questo può essere fatto anche localmente, tramite 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

Dove registry.gitlab.com/kvaps/docs.example.org/website — è il nome della vostra immagine docker, dopo la build verrà automaticamente inviata al registry docker.

Caratteristica —cache consente di memorizzare i layer nel docker registry, per l'esempio fornito verranno salvati in registry.gitlab.com/kvaps/docs.example.org/website/cache, ma puoi anche specificare un percorso diverso utilizzando il parametro --cache-repo.

Screenshot del docker-registry

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

4. Introduzione a qbec

Qbec è uno strumento di deployment che permette di descrivere in modo dichiarativo i manifesti della tua applicazione e di deployarli in Kubernetes. L'uso di Jsonnet come sintassi principale semplifica enormemente la descrizione delle differenze per diversi ambienti e riduce quasi completamente la ripetizione del codice.

Questo può essere particolarmente utile nei casi in cui devi deployare un'applicazione in più cluster con parametri diversi e desideri descriverli in modo dichiarativo in Git.

Qbec consente anche di renderizzare chart Helm passando i parametri necessari e, in seguito, di operare su di essi come su normali manifesti; è possibile applicare diverse mutazioni, il che a sua volta elimina la necessità di utilizzare ChartMuseum. In altre parole, puoi memorizzare e renderizzare i chart direttamente da git, dove dovrebbero trovarsi.

Come ho già detto, tutte le distribuzioni saranno conservate nella directory deploy/:

mkdir deploy
cd deploy

Iniziamo a inizializzare la nostra prima applicazione:

qbec init website
cd website

Attualmente, la struttura della nostra applicazione appare così:

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

Diamo un'occhiata al file qbec.yaml:

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

Qui ci interessa principalmente spec.environments, qbec ha già creato per noi l'ambiente default e ha preso l'indirizzo del server, così come il namespace dal nostro attuale kubeconfig.
Ora, durante il deployment in default l'ambiente, qbec deployerà sempre solo nel cluster Kubernetes specificato e nel namespace specificato, quindi non dovrete più passare tra contesti e namespaces per effettuare un deployment.
Se necessario, potete sempre aggiornare le impostazioni in questo file.

Tutti i vostri ambienti sono descritti in qbec.yaml, e nel file params.libsonnet, dove è specificato da dove prendere i parametri per essi.

Successivamente vediamo due directory:

  • components/ — qui verranno archiviati tutti i manifesti per la nostra applicazione, che possono essere descritti sia in jsonnet che in normali file yaml
  • environments/ — qui descriveremo tutte le variabili (parametri) per i nostri ambienti.

Per impostazione predefinita, abbiamo due file:

  • environments/base.libsonnet — conterrà parametri comuni a tutti gli ambienti
  • environments/default.libsonnet — contiene parametri sovrascritti per l'ambiente default

Apriamo environments/base.libsonnet e aggiungiamo lì i parametri per il nostro primo componente:

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

Creiamo anche il nostro primo componente components/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,
                },
              },
            ],
          },
        },
      ],
    },
  },
]

In questo file abbiamo descritto tre entità Kubernetes: Deployment, Service e Ingress. Se lo desideriamo, potremmo separarli in componenti diversi, ma per ora ci basta uno solo.

Sintassi jsonnet assomiglia molto al normale json; in effetti, un json normale è già un jsonnet valido, quindi inizialmente potrebbe essere più facile utilizzare servizi online come yaml2json per convertire il vostro yaml conosciuto in json, oppure, se i vostri componenti non contengono variabili, possono essere descritti semplicemente come un normale yaml.

Quando lavorate con jsonnet vi consiglio vivamente di installare un plug-in per il vostro editor.

Ad esempio, per vim c'è il plugin vim-jsonnet, che include la evidenziazione della sintassi e esegue automaticamente jsonnet fmt ad ogni salvataggio (richiede l'installazione di jsonnet).

Tutto è pronto, ora possiamo iniziare il deployment:

Per vedere cosa abbiamo ottenuto, eseguiamo:

qbec show default

In output vedrete i manifesti yaml renderizzati, che saranno applicati al cluster default.

Ottimo, ora applichiamo:

qbec apply default

In output vedrete sempre cosa verrà fatto nel vostro cluster, qbec vi chiederà di confermare le modifiche digitando y potrete confermare le vostre intenzioni.

Pronto, ora la nostra applicazione è stata distribuita!

In caso di modifiche, potrete sempre eseguire:

qbec diff default

per vedere come queste modifiche influenzeranno il deployment attuale

Non dimentichiamo di commettere le nostre modifiche:

cd ../..
git add deploy/website
git commit -m "Aggiungi deployment per il sito web"

5. Proviamo Gitlab-runner con Kubernetes-executor

Fino a poco tempo fa, utilizzavo solo il normale gitlab-runner su una macchina preparata in anticipo (contenitore LXC) con shell- o docker-executor. Inizialmente avevamo alcuni di questi runner globalmente definiti nel nostro GitLab. Essi creavano immagini Docker per tutti i progetti.

Ma come ha dimostrato la pratica, questa opzione non è la più ideale, sia in termini di praticità che di sicurezza. È molto meglio e ideologicamente più corretto avere runner separati distribuiti per ogni progetto, o addirittura per ogni ambiente.

Fortunatamente, questo non è affatto un problema, poiché ora distribuiremo gitlab-runner direttamente come parte del nostro progetto direttamente in Kubernetes.

Gitlab fornisce un helm-chart già pronto per distribuire gitlab-runner in Kubernetes. Quindi, tutto ciò che dovete fare è scoprire registration token per il nostro progetto in Impostazioni —> CI / CD —> Runners e inserirlo in 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

Dove:

  • https://gitlab.com — l'indirizzo del tuo server GitLab.
  • yga8y-jdCusVDn_t4Wxc — token di registrazione per il tuo progetto.
  • rbac.create=true — fornisce al runner le necessarie autorizzazioni per poter creare pod per eseguire le nostre attività utilizzando kubernetes-executor.

Se tutto è stato fatto correttamente, dovresti vedere il runner registrato nella sezione Runners, nelle impostazioni del tuo progetto.

Screenshot del runner aggiunto

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

È davvero così semplice? — sì, così semplice! Niente più complicazioni con la registrazione manuale dei runner, da questo momento in poi i runner saranno creati e distrutti automaticamente.

6. Deploy dei chart Helm con QBEC

Poiché abbiamo deciso di considerarlo gitlab-runner parte del nostro progetto, è giunto il momento di descriverlo nel nostro repository Git.

Potremmo descriverlo come un componente separato website, ma in futuro prevediamo di fare il deploy di diverse copie website molto frequentemente, a differenza gitlab-runnerdi quello che sarà deployato solo una volta per ogni cluster Kubernetes. Quindi iniziamo a inizializzare un'app separata per esso:

cd deploy
qbec init gitlab-runner
cd gitlab-runner

Questa volta non descriveremo manualmente le entità di Kubernetes, ma utilizzeremo un chart Helm già pronto. Uno dei vantaggi di qbec è la possibilità di renderizzare chart Helm direttamente da un repository Git.

Colleghiamolo usando git submodule:

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

Ora la directory vendor/gitlab-runner contiene un repository con il chart per gitlab-runner.

In modo simile, è possibile collegare altri repository, ad esempio l'intero repository con i chart ufficiali. https://github.com/helm/charts

Descriviamo il componente 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,
  }
)

Il primo argomento per expandHelmTemplate è il percorso al chart, poi passiamo params.values, che prenderemo dai parametri dell'ambiente, seguito da un oggetto con

  • nameTemplate — il nome del rilascio
  • namespace — lo namespace passato a Helm
  • thisFile — parametro obbligatorio che fornisce il percorso al file corrente
  • verbose — mostra il comando helm template con tutti gli argomenti durante il rendering del chart

Ora descriviamo i parametri per il nostro componente in 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,
      },
    },
  },
}

Attenzione runnerRegistrationToken lo prendiamo da un file esterno secrets/base.libsonnet, allora creiamolo:

{
  runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}

Controlliamo se tutto funziona:

qbec show default

se tutto è a posto, possiamo eliminare il nostro rilascio precedente, installato tramite Helm:

helm uninstall gitlab-runner

e reinstallarlo, ma già tramite qbec:

qbec apply default

7. Introduzione a git-crypt

Git-crypt è uno strumento che consente di impostare la crittografia trasparente per il tuo repository.

Attualmente la struttura della nostra directory per gitlab-runner appare così:

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

Ma tenere segreti in Git non è sicuro, giusto? Quindi dobbiamo crittografarli correttamente.

Di solito, per una sola variabile non ha sempre senso. Puoi passare i segreti in qbec e attraverso le variabili ambientali del tuo sistema CI.
Tuttavia, va notato che ci sono progetti più complessi, che possono contenere molti più segreti, e trasmetterli tutti tramite variabili di ambiente sarà estremamente difficile.

Inoltre, in quel caso non sarei stato in grado di parlarvi di uno strumento così fantastico come git-crypt.

git-crypt è comodo anche perché consente di mantenere tutta la storia dei segreti, oltre a confrontare, unire e risolvere conflitti proprio come siamo abituati a fare con Git.

La prima cosa da fare dopo l'installazione git-crypt è generare le chiavi per il nostro repository:

git crypt init

Se hai una chiave PGP, puoi subito aggiungerti come collaboratore per questo progetto:

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

In questo modo, potrai sempre decrittografare questo repository utilizzando la tua chiave privata.

Se non hai una chiave PGP e non prevedi di averne, puoi seguire un percorso diverso ed esportare la chiave del progetto:

git crypt export-key /path/to/keyfile

In questo modo, chiunque possieda il file di chiave esportato keyfile potrà decrittografare il tuo repository.

È ora di configurare il nostro primo segreto.
Ricordo che siamo ancora nella directory deploy/gitlab-runner/, dove abbiamo una directory secrets/, ora crittografiamo tutti i file in essa, per questo creiamo un file secrets/.gitattributes con il seguente contenuto:

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

Come si può vedere dal contenuto, tutti i file corrispondenti alla maschera * verranno elaborati tramite git-crypt, ad eccezione di .gitattributes

Possiamo verificarlo eseguendo:

git crypt status -e

In uscita otterremo un elenco di tutti i file nel repository per i quali è attivata la crittografia

Questo è tutto, ora possiamo tranquillamente impegnare le nostre modifiche:

cd ../..
git add .
git commit -m "Aggiungi il deploy per gitlab-runner"

Per bloccare il repository è sufficiente eseguire:

git crypt lock

e subito tutti i file crittografati si trasformeranno in un insieme binario, sarà impossibile leggerli.
Per decrittografare il repository, eseguire:

git crypt unlock

8. Creiamo un'immagine toolbox

L'immagine toolbox è un'immagine con tutti gli strumenti che utilizzeremo per il deploy del nostro progetto. Sarà usata dal gitlab-runner per svolgere compiti di deploy standard.

Qui è tutto semplice, creiamo un nuovo dockerfiles/toolbox/Dockerfile con il seguente contenuto:

DA 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

Come potete notare, in questa immagine stiamo installando tutte le utility che abbiamo usato per il deploy della nostra applicazione. Qui non ci serve, a meno che non kubectl, ma forse vorrete giocarci nella fase di configurazione del pipeline.

Inoltre, per poter comunicare con Kubernetes e effettuare il deploy, dobbiamo configurare un ruolo per i pod generati dal gitlab-runner.

Per fare ciò, ci sposteremo nella directory del gitlab-runner:

cd deploy/gitlab-runner

e aggiungeremo un nuovo componente 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,
      },
    ],
  },
]

Descriviamo anche i nuovi parametri in environments/base.libsonnet, che ora appare così:

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

Attenzione $.components.rbac.name fa riferimento a name per il componente rbac

Verifichiamo cosa è cambiato:

qbec diff default

e applichiamo le nostre modifiche in Kubernetes:

qbec apply default

Non dimentichiamo di fare il commit delle nostre modifiche in git:

cd ../..
git add dockerfiles/toolbox
git commit -m "Aggiungi Dockerfile per toolbox"
git add deploy/gitlab-runner
git commit -m "Configura gitlab-runner per usare toolbox"

9. Il nostro primo pipeline e la costruzione delle immagini per tag

Nella radice del progetto creeremo .gitlab-ci.yml con il seguente contenuto:

.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

Si prega di notare che stiamo usando GIT_SUBMODULE_STRATEGY: normal per quei job in cui è necessario inizializzare esplicitamente i submoduli prima dell'esecuzione.

Non dimentichiamo di commettere le nostre modifiche:

git add .gitlab-ci.yml
git commit -m "Automatizza la costruzione di docker"

Penso che possiamo tranquillamente definirlo una versione v0.0.1 e aggiungere il tag:

git tag v0.0.1

I taggheremo ogni volta che ci sarà bisogno di rilasciare una nuova versione. I tag nelle immagini Docker saranno collegati ai tag di Git. Ogni push con un nuovo tag inizierà la costruzione delle immagini con quel tag.

Eseguiamo git push --tags, e diamo un'occhiata al nostro primo pipeline:

Screenshot del primo pipeline

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Vale la pena notare che la costruzione per i tag è adatta per la costruzione delle immagini Docker, ma non è adatta per il deployment delle applicazioni in Kubernetes. Poiché nuovi tag possono essere assegnati anche a commit più vecchi, in questo caso l'inizializzazione del pipeline per essi porterà al deploy di una versione precedente.

Per risolvere questo problema, solitamente la costruzione delle immagini Docker è legata ai tag, mentre il deployment dell'applicazione è legato al branch master, in cui sono hardcoded le versioni delle immagini costruite. Solo in questo caso potrai iniziare un rollback semplicemente revertendo master-il branch.

10. Automazione del deployment

Affinché Gitlab-runner possa decifrare i nostri segreti, dovremo esportare la chiave del repository e aggiungerla alle variabili ambientali della nostra CI:

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

salviamo la stringa ottenuta in Gitlab, per fare ciò accediamo alle impostazioni del nostro progetto:
Impostazioni —> CI / CD —> Variabili

E creiamo una nuova variabile:

Tipo
Chiave
Valore
Protetto
Mascherato
Ambito

File
GITCRYPT_KEY
<la tua stringa>
true (per il periodo di apprendimento si può anche false)
true
Tutti gli ambienti

Screenshot della variabile aggiunta

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Ora aggiorniamo il nostro .gitlab-ci.yml aggiungendo in esso:

.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

Qui abbiamo utilizzato diverse nuove opzioni per qbec:

  • --root some/app -- consente di definire la directory specifica dell'applicazione
  • --force:k8s-context __incluster__ -- è una variabile magica che indica che il deploy avverrà nel mismo cluster in cui è stato avviato il gitlab-runner. È necessario farlo, altrimenti qbec cercherà di trovare un server Kubernetes appropriato nel tuo kubeconfig
  • --wait -- costringe qbec ad attendere che le risorse create raggiungano lo stato Ready e solo allora terminerà con un exit-code di successo.
  • --yes -- disabilita semplicemente la shell interattiva Sei sicuro? durante il deploy.

Non dimentichiamo di commettere le nostre modifiche:

git add .gitlab-ci.yml
git commit -m "Automatizza il deploy"

E dopo git push vedremo come le nostre applicazioni sono state distribuite:

Screenshot del secondo pipeline

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

11. Artefatti e build al push su master

Di solito, i passaggi sopra descritti sono sufficienti per la build e la distribuzione di quasi qualsiasi microservizio, ma non vogliamo aggiungere un tag ogni volta che dobbiamo aggiornare il sito. Pertanto, seguiremo un approccio più dinamico e configureremo il deploy tramite digest nel ramo master.

L'idea è semplice: ora l'immagine del nostro website verrà ricompilata ogni volta che eseguiamo un push in master, e dopo verrà automaticamente distribuita in Kubernetes.

Aggiorniamo queste due job nel nostro .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"

Si prega di notare che abbiamo aggiunto il ramo master a refs per il job build_website e ora stiamo usando $CI_COMMIT_REF_NAME invece di $CI_COMMIT_TAG, ovvero ci scollegiamo dai tag in Git e ora procederemo a pushare un'immagine con il nome del ramo del commit che ha avviato il pipeline. Vale la pena notare che questo funziona anche con i tag, permettendoci di salvare gli snapshot del sito con una versione specifica nel docker-registry.

Quando il nome del docker tag per la nuova versione del sito può rimanere invariato, dobbiamo comunque descrivere le modifiche per Kubernetes, altrimenti non riesce a ridistribuire l'applicazione dalla nuova immagine, poiché non noterà alcuna modifica nel manifesto di distribuzione.

Opzione —vm:ext-str digest=»$DIGEST» per qbec — consente di passare una variabile esterna in jsonnet. Vogliamo che con ogni rilascio della nostra applicazione venga ridistribuita nel cluster. Non possiamo più utilizzare il nome del tag, che ora potrebbe rimanere invariato, poiché abbiamo bisogno di fare riferimento a una versione specifica dell'immagine e triggerare la distribuzione al suo cambiamento.

Qui ci aiuterà la possibilità di Kaniko di salvare il digest dell'immagine in un file (opzione —digest-file)
Poi passeremo e leggeremo questo file al momento della distribuzione.

Aggiorneremo i parametri per il nostro deploy/website/environments/base.libsonnet che ora apparirà così:

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

Tutto pronto, ora ogni commit in master inizializzerà la build dell'immagine docker per website, e successivamente la sua distribuzione in Kubernetes.

Non dimentichiamo di commettere le nostre modifiche:

git add .
git commit -m "Configura build dinamico"

Controlliamo, dopo git push dovremmo vedere qualcosa di simile:

Screenshot della pipeline per master

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Di fatto non è necessario ridistribuire gitlab-runner ad ogni push, a meno che non ci siano stati cambiamenti nella sua configurazione, correggiamo questo in .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 permetterà di monitorare le modifiche in deploy/gitlab-runner/ e attiverà il nostro job solo se ce ne sono

Non dimentichiamo di commettere le nostre modifiche:

git add .gitlab-ci.yml
git commit -m "Riduci la distribuzione di gitlab-runner"

git push, va sicuramente meglio:

Screenshot della pipeline aggiornata

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

12. Ambienti dinamici

È tempo di rendere la nostra pipeline più variegata con ambienti dinamici.

Iniziamo ad aggiornare il job build_website nel nostro .gitlab-ci.yml, rimuovendo il blocco only, che farà scattare GitLab al commit in qualsiasi ramo:

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/

Poi aggiorniamo il job deploy_website, aggiungiamo un blocco 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"

Questo permetterà a GitLab di associare il job a prod un ambiente e visualizzare il corretto link ad esso.

Ora aggiungiamo altre due job:

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

Saranno avviati con push su qualsiasi branch tranne master e implementeranno una versione di anteprima del sito.

Vediamo una nuova opzione per qbec: —app-tag — consente di contrassegnare le versioni distribuite dell'applicazione e di operare solo all'interno di quel tag; durante la creazione e la distruzione delle risorse in Kubernetes, qbec si occuperà solo di esse.
In questo modo possiamo evitare di creare un ambiente separato per ogni review e semplicemente riutilizzare lo stesso.

Qui utilizziamo anche qbec apply review, invece qbec apply default — questo è proprio il momento in cui cercheremo di descrivere le differenze tra i nostri ambienti (review e default):

Aggiungeremo review l'ambiente in deploy/website/qbec.yaml

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

Poi lo dichiariamo in 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

E scriviamo i parametri personalizzati per esso in 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',
    },
  },
}

Diamo anche un'occhiata più da vicino al job stop_review, verrà attivato quando eliminiamo il branch e affinché gitlab non cerchi di fare il checkout su di esso si utilizza GIT_STRATEGY: none, in seguito cloniamo master-il branch e rimuoviamo review attraverso di esso.
Un po' complicato, ma al momento non ho trovato un modo più elegante.
Un'alternativa potrebbe essere il deploy di ogni review in uno spazio dei nomi separato, che può sempre essere completamente rimosso.

Non dimentichiamo di commettere le nostre modifiche:

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

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

Screenshot degli environments creati in Gitlab

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Funziona tutto? — ottimo, eliminiamo il nostro branch di test: git checkout master, git push origin :test, verifichiamo che i job per la rimozione dell'environment siano stati eseguiti senza errori.

Qui vorrei sottolineare che qualsiasi sviluppatore nel progetto può creare branch e può anche modificare .gitlab-ci.yml il file e accedere alle variabili segrete.
Pertanto, è fortemente consigliato consentire il loro utilizzo solo per i branch protetti, ad esempio in master, oppure creare un set di variabili separato per ogni ambiente.

13. Review Apps

App di revisione è una funzionalità di Gitlab che consente di aggiungere un pulsante per la visualizzazione rapida di ogni file nel repository nell'ambiente implementato.

Per fare in modo che questi pulsanti appaiano, è necessario creare un file .gitlab/route-map.yml e descrivere in esso tutte le trasformazioni dei percorsi, nel nostro caso sarà molto semplice:

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

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

Non dimentichiamo di commettere le nostre modifiche:

git add .gitlab/
git commit -m "Enable review apps"

git push, e verifichiamo:

Screenshot del pulsante Review App

Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Job concluso!

Sorgenti del progetto:

Grazie per l'attenzione, spero che vi sia piaciuto Proviamo nuovi strumenti per la creazione e l'automazione del deploy in Kubernetes

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster