
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 , che viene generato da 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
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.orgE allo stesso modo un repository git:
cd docs.example.org
git initFinora 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 , 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/learnIn 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 serverE all'indirizzo 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

Per generare il sito basta eseguire:
hugoIl contenuto della directory public/ sarà il vostro sito.
Sì, a proposito, mettiamolo immediatamente in .gitignore:
echo /public > .gitignoreNon 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 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 , 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.1Dove 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

4. Introduzione a 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 deployIniziamo a inizializzare la nostra prima applicazione:
qbec init website
cd websiteAdesso la struttura della nostra applicazione è la seguente:
.
├── components
├── environments
│ ├── base.libsonnet
│ └── default.libsonnet
├── params.libsonnet
└── qbec.yamldiamo 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 defaultIn output vedrete i manifesti yaml renderizzati, che verranno applicati al cluster di default.
Ottimo, ora applica:
qbec apply defaultIn 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 defaultper 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-runnerDove:
- — 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

È 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-runnerQuesta 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-runnerOra 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.
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 defaultse va tutto bene, possiamo rimuovere il nostro rilascio precedentemente distribuito tramite Helm:
helm uninstall gitlab-runnere ridistribuirlo, ma già tramite qbec:
qbec apply default7. Introduzione a 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 initSe hai una chiave PGP, puoi subito aggiungerti come collaboratore per questo progetto:
git-crypt add-gpg-user kvapss@gmail.comIn 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/keyfileIn 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 !diffCome 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 -eOtterremo 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 locke subito tutti i file crittografati diventeranno un'entità binaria, sarà impossibile leggerli.
Per decrittare il repository, esegui:
git crypt unlock8. 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/helmCome 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-runnere 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 defaulte applichiamo le nostre modifiche in Kubernetes:
qbec apply defaultNon 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:
- tagsSi 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.1Attaccheremo 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

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

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

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

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

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: manualSaranno 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:8443Poi 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.thisFileE 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

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

Lavoro completato!
Fonti del progetto:
- su Gitlab:
- su GitHub:
Grazie per l'attenzione, spero vi sia piaciuto ![]()
Fonte: habr.com
