Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

Ciao! Negli ultimi tempi sono emersi molti strumenti interessanti per l'automazione sia della creazione di immagini Docker che per il deployment in Kubernetes. Pertanto, ho deciso di sperimentare con GitLab, approfondire le sue funzionalità e, ovviamente, impostare un pipeline.

L'ispirazione per questo lavoro è venuta dal sito kubernetes.io, che viene generato da codici sorgente automaticamente, e per ogni pull request inviata, il robot genera automaticamente una versione anteprima del sito con le vostre modifiche e fornisce un link per la visualizzazione.

Ho cercato di costruire un processo simile da zero, interamente basato su GitLab CI e su strumenti gratuiti che uso abitualmente per il deployment di applicazioni in Kubernetes. Oggi, finalmente, vi parlerò di loro in dettaglio.

Nell'articolo verranno analizzati 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 Kubernetes-executor
  6. Deploy di Helm-charts 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 deployment
  11. Artefatti e costruzione durante il push in master
  12. Ambientazioni dinamiche
  13. Review Apps

1. Introduzione a Hugo

Come esempio per il nostro progetto, cercheremo di creare un sito per la pubblicazione della documentazione basato su Hugo. Hugo è un generatore statico di contenuti.

Per coloro che non sono familiari con i generatori statici, parlerò di loro in modo più dettagliato. A differenza dei comuni motori per siti con database e PHP, che generano pagine al volo su richiesta dell'utente, i generatori statici funzionano in modo un po' diverso. Permettono di prendere i file sorgente, generalmente un insieme di file in formato Markdown e i modelli di tema, e quindi compilarli in un sito completamente pronto.

Quindi, come risultato finale 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 testato:

Inizializziamo un nuovo sito:

hugo new site docs.example.org

E allo stesso modo un repository git:

cd docs.example.org
git init

Finora il nostro sito è totalmente pulito e per far sì che qualcosa appaia, prima dobbiamo collegare un tema, che è semplicemente un insieme di template e regole stabilite secondo le quali il nostro sito viene generato.

Come tema utilizzeremo Learn, che, a mio avviso, è perfetta per un sito con documentazione.

È importante notare che non è necessario salvare 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 collegati al nostro progetto, mentre il tema collegato rimarrà come un collegamento a un repository specifico e al commit in esso, cioè sarà sempre possibile estrarlo dalla fonte originale senza temere modifiche incompatibili.

Modifichiamo il config config.toml:

baseURL = "http://docs.example.org/"
languageCode = "en-us"
title = "Il mio sito di documentazione"
theme = "learn"

Già a questo punto possiamo avviare:

hugo server

E all'indirizzo http://localhost:1313/ controllare il nostro sito appena creato, tutte le modifiche effettuate nella directory si aggiornano automaticamente e la pagina aperta nel browser, molto comodo!

Proviamo a creare una pagina di copertura 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 compilazione e l'automazione del deployment in Kubernetes.

Per generare il sito basta eseguire:

hugo

Il contenuto della directory public/ sarà il vostro sito.
Sì, a proposito, mettiamolo immediatamente in .gitignore:

echo /public > .gitignore

Non dimentichiamo di fare il commit delle nostre modifiche:

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

2. Preparazione del Dockerfile

È tempo di definire la struttura del nostro repository. Di solito utilizzo 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 deployment delle nostre applicazioni in Kubernetes

In questo modo, creeremo il nostro primo Dockerfile in 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 possibilità è chiamata multi-stage build e consente di escludere dal docker-image finale tutto ciò che non è necessario.
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 fare il commit delle nostre modifiche:

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

3. Introduzione a kaniko

Come assemblatore di immagini docker ho deciso di utilizzare kaniko, poiché per il suo funzionamento non è necessaria la presenza di un demone docker e l'assemblaggio può avvenire su qualsiasi macchina, memorizzando la cache direttamente nel registro, eliminando così la necessità di avere uno storage persistente completo.

Per assemblare l'immagine, basta avviare un contenitore con kaniko executor e fornirgli il contesto corrente di assemblaggio, il che è possibile 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 tua immagine docker, dopo l'assemblaggio verrà automaticamente caricata nel registro docker.

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

Screenshot del docker-registry

Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

4. Introduzione a qbec

Qbec è uno strumento di deployment che consente di descrivere in modo dichiarativo i manifesti della tua applicazione e di distribuirli in Kubernetes. L'uso di Jsonnet come sintassi principale semplifica notevolmente la descrizione delle differenze per più ambienti e praticamente elimina la ripetizione del codice.

Questo può essere particolarmente rilevante nei casi in cui è necessario distribuire un'applicazione in più cluster con parametri diversi e si desidera descriverli in modo dichiarativo su Git.

Qbec permette anche di eseguire il rendering dei grafici Helm passando i parametri necessari e in seguito di operarli come normali manifesti, incluso il fatto che è possibile applicare loro diverse mutazioni, il che, a sua volta, elimina la necessità di utilizzare ChartMuseum. Questo significa che puoi memorizzare e rendere i grafici direttamente da git, dove hanno il loro posto.

Come ho detto in precedenza, tutte le distribuzioni verranno memorizzate nella directory deploy/:

mkdir deploy
cd deploy

Iniziamo a inizializzare la nostra prima applicazione:

qbec init website
cd website

Adesso la struttura della nostra applicazione è la seguente:

.
├── 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 in primo luogo spec.environments, qbec ha già creato per noi un ambiente di default e ha preso l'indirizzo del server, così come il namespace dal nostro attuale kubeconfig.
Ora durante il deploy in default l'ambiente, qbec eseguirà sempre il deploy solo nel cluster Kubernetes specificato e nel namespace specificato, quindi non sarà più necessario passare tra contesti e namespace per eseguire il deploy.
In caso di necessità, puoi sempre aggiornare le impostazioni in questo file.

Tutti i tuoi 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 saranno conservati tutti i manifesti per la nostra applicazione, 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 per tutti gli ambienti
  • environments/default.libsonnet — contiene parametri sovrascritti per l'ambiente default

Apriamo environments/base.libsonnet e aggiungiamo 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, Servizio e Ingress. Se lo desideriamo, potremmo separarle in componenti distinti, ma per ora ci basta una sola.

Sintassi jsonnet è molto simile a un normale json; in effetti, un normale json è già un jsonnet valido, quindi all'inizio potrebbe essere più semplice utilizzare servizi online come yaml2json per convertire il vostro yaml familiare in json, oppure, se i vostri componenti non contengono variabili, possono essere semplicemente descritti come un normale yaml.

Lavorando con jsonnet Consiglio vivamente di installare un plugin per il vostro editor.

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

Tutto è pronto, ora possiamo iniziare il deployment:

Per vedere cosa abbiamo ottenuto, eseguiamo:

qbec show default

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

Ottimo, ora applica:

qbec apply default

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

Fatto, ora la nostra applicazione è stata deployata!

In caso di modifiche, potrai sempre eseguire:

qbec diff default

per vedere come queste modifiche influenzeranno il deployment attuale

Non dimentichiamo di fare il commit delle 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 un sistema standard gitlab-runner su una macchina preconfigurata (contenitore LXC) con shell- o docker-executor. Inizialmente avevamo alcuni di questi runner globalmente definiti nel nostro Gitlab. Raccollevano immagini docker per tutti i progetti.

Ma come ha dimostrato la pratica, questa opzione non è la più ideale, sia dal punto di vista pratico che della sicurezza. È molto meglio e ideologicamente corretto avere runner separati distribuiti per ogni progetto, se non per ogni ambiente.

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

Gitlab fornisce un chart helm pronto per distribuire gitlab-runner in Kubernetes. Dunque, tutto ciò che devi fare è scoprire registration token per il nostro progetto in Impostazioni —> CI \/ CD —> Runners e passarci il 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 — indirizzo del tuo server Gitlab.
  • yga8y-jdCusVDn_t4Wxc — registration token per il tuo progetto.
  • rbac.create=true — fornisce al runner il numero necessario di privilegi per poter creare pod per eseguire i nostri compiti con il 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 compilazione e l'automazione del deployment in Kubernetes.

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

6. Deployment di Helm-charts con QBEC

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

Potremmo descriverlo come un componente separato website, ma in seguito prevediamo di distribuire diverse copie website molto frequentemente, a differenza gitlab-runner, che verrà distribuito solo una volta per ogni cluster Kubernetes. Dunque, iniziamo a inizializzare una applicazione separata per esso:

cd deploy
qbec init gitlab-runner
cd gitlab-runner

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

Colleghiamolo usando un sottodominio git:

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

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

Allo stesso modo possiamo 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,
  }
)

Come primo argomento a expandHelmTemplate passiamo il percorso al chart, poi params.values, che prenderemo dai parametri dell'ambiente, poi segue un oggetto con

  • nameTemplate — il nome del rilascio
  • namespace — il namespace passato a Helm
  • thisFile — un parametro obbligatorio che passa 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,
      },
    },
  },
}

Si prega di notare runnerRegistrationToken lo preleviamo da un file esterno secrets/base.libsonnet, creiamo questo file:

{
  runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}

Verifichiamo se tutto funziona:

qbec show default

se va tutto bene, possiamo rimuovere il nostro rilascio precedentemente distribuito tramite Helm:

helm uninstall gitlab-runner

e ridistribuirlo, ma già tramite qbec:

qbec apply default

7. Introduzione a git-crypt

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

Al momento 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 conservare segreti in Git non è sicuro, giusto? Quindi dobbiamo crittografarli correttamente.

Di solito, per una sola variabile, non sempre ha senso. Puoi passare segreti in qbec e tramite variabili d'ambiente del tuo sistema CI.
Ma è importante notare che ci sono anche progetti più complessi che possono contenere molti più segreti, trasferirli tutti tramite variabili ambientali sarebbe estremamente difficile.

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

git-crypt è anche utile perché consente di mantenere la cronologia di tutti i segreti, oltre a confrontare, unire e risolvere i 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 decrittare questo repository utilizzando la tua chiave privata.

Se non hai una chiave PGP e non ne prevedi una, puoi seguire un altro percorso ed esportare la chiave del progetto:

git crypt export-key /path/to/keyfile

In questo modo chiunque possieda la chiave esportata keyfile potrà decrittare il tuo repository.

È tempo di configurare il nostro primo segreto.
Ricordo che ci troviamo ancora nella directory deploy/gitlab-runner/, dove abbiamo una directory secrets/, quindi andiamo a crittografare tutti i file in essa, per fare ciò creiamo un file secrets/.gitattributes con questo contenuto:

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

Come si può vedere dal contenuto, tutti i file che corrispondono al modello * saranno elaborati tramite git-crypt, ad eccezione dello stesso .gitattributes

Possiamo verificare ciò eseguendo:

git crypt status -e

Otterremo un elenco di tutti i file nel repository per i quali è attivata la crittografia.

Ecco fatto, ora possiamo tranquillamente effettuare il commit delle nostre modifiche:

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

Per bloccare il repository basta eseguire:

git crypt lock

e subito tutti i file crittografati diventeranno un'entità binaria, sarà impossibile leggerli.
Per decrittare il repository, esegui:

git crypt unlock

8. Creiamo un'immagine toolbox

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

Qui è tutto semplice, creiamo un nuovo dockerfiles/toolbox/Dockerfile con questo 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 installiamo tutte le utility che abbiamo usato per il deployment della nostra applicazione. Qui non abbiamo bisogno di kubectl, ma forse vorreste giocarci durante la fase di configurazione del pipeline.

Inoltre, per poter comunicare con Kubernetes ed eseguire il deployment, dobbiamo configurare un ruolo per i pod generati dal gitlab-runner.

Per questo, andiamo nella directory con il gitlab-runner:

cd deploy/gitlab-runner

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

Si prega di notare $.components.rbac.name fa riferimento a name per il componente rbac

Controlliamo 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 utilizzare toolbox"

9. Il nostro primo pipeline e il build delle immagini per tag

Nella radice del progetto creeremo .gitlab-ci.yml con questo 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 fare il commit delle nostre modifiche:

git add .gitlab-ci.yml
git commit -m "Automatizza la build del docker"

Penso che possiamo tranquillamente chiamarlo versione v0.0.1 e attaccare un tag:

git tag v0.0.1

Attaccheremo i tag ogni volta che avremo bisogno di rilasciare una nuova versione. I tag nelle immagini Docker saranno legati ai tag Git. Ogni push con un nuovo tag inizializzerà la build 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 compilazione e l'automazione del deployment in Kubernetes.

Vale la pena notare che la build dei tag è adatta per le immagini docker, ma non è adatta per il deployment dell'applicazione in Kubernetes. Poiché nuovi tag possono essere assegnati anche a vecchi commit, in questo caso l'inizializzazione del pipeline per essi porterà al deployment della vecchia versione.

Per risolvere questo problema, di solito la build delle immagini docker è legata ai tag, mentre il deployment dell'applicazione è legato al ramo master, dove le versioni delle immagini costruite sono hardcodificate. In questo caso, sarà possibile inizializzare un rollback semplicemente con un revert master-del ramo.

10. Automazione del deployment

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

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

salveremo la stringa ottenuta in Gitlab, quindi andiamo alle impostazioni del nostro progetto:
Impostazioni —> CI / CD —> Variabili

E creeremo una nuova variabile:

Type
Key
Value
Protetto
Mascherato
Ambito

File
GITCRYPT_KEY
<your string>
true (per il periodo di apprendimento può anche essere false)
true
Tutti gli ambienti

Screenshot della variabile aggiunta

Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

Ora aggiorneremo il nostro .gitlab-ci.yml aggiungendo a 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 specificare la directory di un'applicazione specifica
  • —force:k8s-context __incluster__ — è una variabile magica che indica che il deploy avverrà nello stesso cluster in cui è in esecuzione gitlab-runner. Questo è necessario poiché altrimenti qbec cercherà un server Kubernetes adatto nella tua kubeconfig
  • —wait — costringe qbec ad attendere che le risorse create passino allo 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 fare il commit delle nostre modifiche:

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

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

Screenshot della seconda pipeline

Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

11. Artefatti e build durante il push su master

Di solito, i passaggi sopra descritti sono sufficienti per costruire e distribuire quasi qualsiasi microservizio, ma non vogliamo aggiungere un tag ogni volta che dobbiamo aggiornare il sito. Pertanto, adotteremo un approccio più dinamico e configureremo il deploy in base al digest nel branch master.

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

Aggiorniamo queste due attività 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"

Nota come abbiamo aggiunto il branch master al refs per l'attività build_website e ora usiamo $CI_COMMIT_REF_NAME anziché $CI_COMMIT_TAG, quindi ci stacchiamo dai tag in Git e ora pubblicheremo l'immagine con il nome del branch del commit che ha avviato la pipeline. Vale la pena notare che questo funzionerà anche con i tag, permettendoci di mantenere snapshot del sito con una versione specifica nel docker-registry.

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

Opzione —vm:ext-str digest=»$DIGEST» Per qbec — consente di passare una variabile esterna a jsonnet. Vogliamo che con ogni rilascio della nostra applicazione venga ridistribuita nel cluster. Non possiamo più usare un nome di tag che ora può rimanere immutabile, poiché dobbiamo legarci a una versione specifica dell'immagine e attivare il deployment al suo cambiamento.

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

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

Fatto, ora ogni commit in master inizierà la costruzione dell'immagine docker per website, e poi il suo deployment in Kubernetes.

Non dimentichiamo di fare il commit delle nostre modifiche:

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

Verifichiamo, dopo git push dovremmo vedere qualcosa di simile:

Screenshot del pipeline per master

Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

In linea di massima non abbiamo bisogno di ridistribuire gitlab-runner a ogni push, a meno che, ovviamente, non ci siano state modifiche nella sua configurazione. Sistemiamo 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 in presenza di esse

Non dimentichiamo di fare il commit delle nostre modifiche:

git add .gitlab-ci.yml
git commit -m "Riduci il deployment di gitlab-runner"

git push, così va meglio:

Screenshot del pipeline aggiornato

Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

12. Ambienti dinamici

È giunto il momento di rendere il nostro pipeline più vario con ambienti dinamici.

Cominciamo col modificare il job build_website nel nostro .gitlab-ci.yml, rimuovendo il blocco only, il che costringerà Gitlab a attivarlo ad ogni 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 lì un blocco ambiente:

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 con prod l'ambiente e di visualizzare il collegamento corretto.

Ora aggiungiamo altre due jobs:

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 eseguiti con un push in qualsiasi ramo tranne master e installeranno una versione di anteprima del sito.

Vediamo una nuova opzione per qbec: —app-tag — permette di etichettare le versioni implementate dell'applicazione e di operare solo all'interno di quell'etichetta, durante la creazione e la distruzione delle risorse in Kubernetes qbec opererà solo su di esse.
In questo modo non dobbiamo creare un ambiente separato per ogni review, ma possiamo semplicemente riutilizzare lo stesso.

Qui utilizziamo anche qbec apply review, invece di qbec apply default — è proprio in questo momento che cerchiamo di descrivere le differenze per i nostri ambienti (review e default):

Aggiungiamo review 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 registriamo 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 si elimina un ramo e affinché gitlab non provi a fare il checkout su di esso, viene utilizzato GIT_STRATEGY: none, successivamente cloniamo master-il ramo e rimuoviamo la review tramite esso.
Un po' complicato, ma non ho trovato un modo più bello.
Un'alternativa potrebbe essere il deploy di ogni review in uno spazio dei nomi dedicato, che può sempre essere eliminato completamente.

Non dimentichiamo di fare il commit delle nostre modifiche:

git add .
git commit -m "Abilita la revisione automatica"

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

Screenshot degli ambienti creati in Gitlab

Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

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

Qui è opportuno chiarire che chiunque nel progetto può creare rami, può anche modificare .gitlab-ci.yml un file e accedere alle variabili segrete.
Pertanto, si consiglia vivamente di consentire il loro utilizzo solo per i rami protetti, ad esempio in master, o di creare un insieme separato di variabili per ogni ambiente.

13. Review Apps

Review Apps è una funzionalità di Gitlab che consente di aggiungere un pulsante per la visualizzazione rapida di ogni file nel repository nell'ambiente distribuito.

Per far apparire questi pulsanti, è 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 fare il commit delle nostre modifiche:

git add .gitlab/
git commit -m "Abilita le review apps"

git push, e controlliamo:

Screenshot del pulsante Review App

Proviamo nuovi strumenti per la compilazione e l'automazione del deployment in Kubernetes.

Lavoro completato!

Fonti del progetto:

Grazie per l'attenzione, spero vi sia piaciuto Proviamo nuovi strumenti per la compilazione e l'automazione del deployment 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