README.md 16.4 KB
Newer Older
Nane Kratzke's avatar
Nane Kratzke committed
1
# Lab 02: Deployment Pipelines as Code
Nane Kratzke's avatar
Nane Kratzke committed
2

Nane Kratzke's avatar
Nane Kratzke committed
3
Deployment Pipelines sind ein wesentlicher Baustein im DevOps Ansatz, um Entwicklungszyklen schnell und agil zu halten. Ziel ist es, Code der in ein Code Repository eingebracht wird, möglichst automatisiert zu integrieren, bauen, testen sowie ggf. in eine Umgebung (häufig Test, Staging, Production) auszubringen.
Nane Kratzke's avatar
Nane Kratzke committed
4

Nane Kratzke's avatar
Nane Kratzke committed
5
Mit jedem Code Push wird also automatisiert geprüft, ob der Code in die bestehende Codebasis integriert werden kann, compilierbar ist, alle Tests passiert und deploybar ist. Auf diese Weise können nur funktionierende Softwarezustände in funktionierende Softwarezustände überführt werden. Entwickler sind so nicht einmal in der Lage Code zu erzeugen, der nicht automatisiert durch die Deployment Pipeline verarbeitbar ist.
Nane Kratzke's avatar
Nane Kratzke committed
6

Nane Kratzke's avatar
Nane Kratzke committed
7
Gemäß dem Everything as Code Ansatz versucht man auch Deployment Pipelines als versionierbaren Code ausdrücken zu können. Es gibt diverse solcher Managed oder Self-hosted Services, die als kommerzielle oder auch als Open Source Software genutzt werden können. Z.B.:
Nane Kratzke's avatar
Nane Kratzke committed
8
9
10
11
12
13

- GitLab CI
- Circle CI
- Travis CI
- Jenkins
- Bitbucket Pipelines
Nane Kratzke's avatar
Nane Kratzke committed
14
- und viele mehr
Nane Kratzke's avatar
Nane Kratzke committed
15

Nane Kratzke's avatar
Nane Kratzke committed
16
Da Gitlab als Open Source Lösung einfach installiert werden kann, werden wir das Prinzip einer Deployment Pipeline as Code am Typvertreter Gitlab CI demonstrieren. Die Ansätze anderer CI/CD Dienste funktionieren aber nach sehr vergleichbaren Konzepten. Die Wahl auf Gitlab CI als Typvertreter erfolgt schlicht und ergreifend auf Basis der guten Verfügbarkeit von Gitlab als Open Source Software und dessen häufigen Einsatz in Cloud-native Kontexten.
Nane Kratzke's avatar
Nane Kratzke committed
17

Nane Kratzke's avatar
Nane Kratzke committed
18
Wer mag, kann dieses Lab auch mittels des Managed Service Gitlab.com nachvollziehen. Hierzu müssen Sie sich allerdings registrieren.
Nane Kratzke's avatar
Nane Kratzke committed
19
20
21

## Inhalt

Nane Kratzke's avatar
Nane Kratzke committed
22
23
24
25
26
27
28
29
30
31
32
33
34
- [Lab 02: Deployment Pipelines as Code](#lab-02-deployment-pipelines-as-code)
  - [Inhalt](#inhalt)
  - [Vorbereitung](#vorbereitung)
  - [Übung 1: Erzeugung von Deployment Pipelines](#übung-1-erzeugung-von-deployment-pipelines)
  - [Übung 2: Weiterreichen von Job Erzeugnissen (Artifacts)](#übung-2-weiterreichen-von-job-erzeugnissen-artifacts)
  - [Übung 3: Informationen in eine Pipeline mittels Umgebungsvariablen geben](#übung-3-informationen-in-eine-pipeline-mittels-umgebungsvariablen-geben)
  - [Übung 4: Nutzung von Images](#übung-4-nutzung-von-images)
  - [Was sollten Sie mitnehmen](#was-sollten-sie-mitnehmen)
  - [Links](#links)

## Vorbereitung

[Forken](https://git.mylab.th-luebeck.de/cloud-native/lab-gitlab/-/forks/new) Sie bitte dieses in Gitlab in Ihren Namensraum.
Nane Kratzke's avatar
Nane Kratzke committed
35

Nane Kratzke's avatar
Nane Kratzke committed
36
## Übung 1: Erzeugung von Deployment Pipelines
Nane Kratzke's avatar
Nane Kratzke committed
37

Nane Kratzke's avatar
Nane Kratzke committed
38
Eine Deployment Pipeline besteht aus einer Sequenz von Stages. Jede Stage kann ein oder mehrere Jobs haben. Alle Jobs innerhalb einer Stage werden parallel und isoliert voneinander ausgeführt. Eine Stage wird nur dann ausgeführt, wenn alle Jobs der vorherigen Stage erfolgreich ausgeführt werden konnten.
Nane Kratzke's avatar
Nane Kratzke committed
39

Nane Kratzke's avatar
Nane Kratzke committed
40
Eine typische Pipeline umfasst häufig die folgenden Stages (grundsätzlich können Pipelines beliebig aussehen, es bietet sich jedoch an bewährten Pipeline Blueprints zu folgen):
Nane Kratzke's avatar
Nane Kratzke committed
41
42
43
44
45

- build (zum Erzeugen von Executables)
- test (zum Testen von Executables)
- deploy (zum Ausbringen von Executables)

Nane Kratzke's avatar
Nane Kratzke committed
46
Solch eine einfache Deployment Pipeline wollen wir nun bauen. Führen Sie hierzu bitte die folgenden Schritte aus:
Nane Kratzke's avatar
Nane Kratzke committed
47
48
49

__Aufgaben:__

Nane Kratzke's avatar
Nane Kratzke committed
50
51
1. Sie finden in diesem geforkten Repository eine leere `.gitlab-ci.yml` Datei an. Diese Datei definiert Ihre Pipeline, die Gitlab mit jedem Push in das Repository automatisch anstößt.
2. Fügen Sie in diese Datei nun bitte folgende Inhalte ein und committen+pushen Sie `.gitlab-ci.yml` in das Repository:
Nane Kratzke's avatar
Nane Kratzke committed
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

    ```yaml
    stages:
        - build
        - test
        - deploy

    job1:
        stage: build
        script:
            - echo "Hello I am job 1"

    job2:
        stage: build
        script:
67
            - echo "Hello I am job 2"
Nane Kratzke's avatar
Nane Kratzke committed
68
69
70
71

    job3:
        stage: test
        script:
72
            - echo "Hello I am job 3"
Nane Kratzke's avatar
Nane Kratzke committed
73
74

    job4:
75
        stage: deploy
Nane Kratzke's avatar
Nane Kratzke committed
76
        script:
77
            - echo "Hello I am job 4"
Nane Kratzke's avatar
Nane Kratzke committed
78
    ```
Nane Kratzke's avatar
Nane Kratzke committed
79
3. Gitlab führt dann automatisch, die so definierte [Pipeline](../../../pipelines) aus.
Nane Kratzke's avatar
Nane Kratzke committed
80
   
Nane Kratzke's avatar
Nane Kratzke committed
81
   ![Pipeline](pipeline.png)
Nane Kratzke's avatar
Nane Kratzke committed
82
4. Klicken Sie auf einen dieser Jobs, dann erhalten Sie den Konsolenoutput des Jobs.
Nane Kratzke's avatar
Nane Kratzke committed
83
   
84
   ![Job console output](job-console.png)
Nane Kratzke's avatar
Nane Kratzke committed
85
86

Eine Pipeline ist also sehr einfach mit einer YAML Datei definierbar. YAML Dateien wiederum sind gut durch Code Versionssysteme versionierbar.
Nane Kratzke's avatar
Nane Kratzke committed
87

Nane Kratzke's avatar
Nane Kratzke committed
88
Das ist eigentlich auch schon das wesentliche Prinzip von einer Deployment Pipeline as Code. Sie sehen an diesem Beispiel allerdings auch bereits weitere Aspekte die typisch für Cloud-native Deployment Ansätze sind.
Nane Kratzke's avatar
Nane Kratzke committed
89
90
91

- Jobs sind eigentlich nichts weiter als Shellskripte, die in einem isolierten Container ausgeführt werden.
- Können alle Jobs einer Stage erfolgreich ausgeführt werden, (exit code == 0) werden die Jobs der nächsten Stage gestartet.
Nane Kratzke's avatar
Nane Kratzke committed
92
- Schlägt ein Job fehl (exit code != 0), wird die nächste Stage nicht gestartet. Sie können das ganz einfach ausprobieren, indem Sie bspw. den Befehl `exit 1` in *job3* ergänzen.
Nane Kratzke's avatar
Nane Kratzke committed
93
94
95
96
97
98
99
    ```yaml
    job3:
        stage: test
        script:
            - echo "Hello I am job 3"
            - exit 1
    ```
100
101
102
    Die Pipeline schlägt dann in job3 in Stage `test` fehl.

    ![Pipeline job failed](pipeline-job-failed.png)
Nane Kratzke's avatar
Nane Kratzke committed
103

Nane Kratzke's avatar
Nane Kratzke committed
104
## Übung 2: Weiterreichen von Job Erzeugnissen (Artifacts)
Nane Kratzke's avatar
Nane Kratzke committed
105

Nane Kratzke's avatar
Nane Kratzke committed
106
Jobs laufen isoliert in einem Container ab, sind also zustandslos oder anders ausgedrückt: Jobs "vergessen" erzeugte Artifakte. Dies ist sicherlich in vielen Fällen nicht sinnvoll. Z.B. sollten durch den Compiler erzeugte `.class` Dateien in einem Java Build Schritt an einen Test Job weitergereicht werden können (ansonsten müsste der Test Job erneut kompilieren). Zum Ende der Pipeline soll vielleicht auch eine `jar`-Datei als Endergebnis der Pipeline bereitgestellt werden können.
107
108
109

Hierfür dienen in Pipelines sogenannte Artefakte. Artefakte sind Job Erzeugnisse, die zwischen Jobs entlang einer Stage Sequenz fließen.

110
__Aufgabe__:
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136

Ändern Sie bitte Ihre Pipeline wie folgt ab:

```yaml
stages:
    - generate
    - consume

job1:
    stage: generate
    script:
        - mkdir build
        - echo "Hello I am job 1" > build/job1-result.txt

job2:
    stage: generate
    script:
        - mkdir build
        - echo "Hello I am job 2" > build/job2-result.txt

job3:
    stage: consume
    script:
        - cat build/*-result.txt
```

Nane Kratzke's avatar
Nane Kratzke committed
137
Die Jobs job1 und job2 lenken ihre Resultate also in zwei Dateien um, die im `build` Verzeichnis gespeichert werden. Wir würden an dieser Stelle erwarten, dass der job3 daher folgende Konsolenausgabe erzeugen sollte:
138

139
```
140
141
142
143
Hello I am job1
Hello I am job2
```

144
145
146
147
148
149
150
151
Tatsächlich schlägt der job3 aber wie folgt fehl:

```
cat: can't open 'build/*-result.txt': No such file or directory
$ cat build/*-result.txt
ERROR: Job failed: exit code 1
```

Nane Kratzke's avatar
Nane Kratzke committed
152
Und dies obwohl die Dateien in job1 und job2 korrekt angelegt wurden. Allerdings in einem Container. Und alle Jobs laufen in isolierten Containern voneinander ab, d.h. job1 kennt job2 und job3 nicht, und job2 kenn job1 sowie job3 nicht, usw.. Wenn man Erzeugnisse eines Jobs anderen Jobs innerhalb einer Pipeline bereitstellen muss, dann kann man dies mittels Artefakten machen.
153
154
155
156
157
158
159
160
161

Um Artefakte zu kennzeichnen, können Sie folgenden Eintrag den Jobs job1 und job2 hinzufügen.

```yaml
artifacts:
    paths:
    - build/
```

Nane Kratzke's avatar
Nane Kratzke committed
162
Diese Artefakte stehen dann allen Jobs in der Pipeline zur Verfügung. Sie müssen darauf achten, dass unterschiedliche Jobs unterschiedlich benannte Artefakte erzeugen, ansonsten überschreiben sich identisch benannte Artefakte gegenseitig.
163
164
165
166
167
168
169

Auch dies können Sie einmal ausprobieren, indem Sie anstelle von 

```
echo "Hello I am job 2" > build/job2-result.txt
```

Nane Kratzke's avatar
Nane Kratzke committed
170
folgendes schreiben (also die Job-Nummern in den Artefaktbezeichnern sowohl in Job1 als auch in Job2 entfernen).
171
172
173
174

```
echo "Hello I am job 2" > build/job-result.txt
```
Nane Kratzke's avatar
Nane Kratzke committed
175

Nane Kratzke's avatar
Nane Kratzke committed
176
Dann werden Sie nur eine Ausgabe von job1 oder job2 bekommen. Welche Ausgabe ist davon abhängig welche Job Artifakte von der Pipeline aus job1 und job2 als letztes gesichert wurden. Gehen Sie davon aus, dass dies nicht deterministisch ist - insbesondere bei parallel blaufenden Jobs.
Nane Kratzke's avatar
Nane Kratzke committed
177

Nane Kratzke's avatar
Nane Kratzke committed
178
179
Weiteres zum Artefakt-Handling finden Sie [hier](https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html).

Nane Kratzke's avatar
Nane Kratzke committed
180
## Übung 3: Informationen in eine Pipeline mittels Umgebungsvariablen geben
Nane Kratzke's avatar
Nane Kratzke committed
181

Nane Kratzke's avatar
Nane Kratzke committed
182
Jobs innerhalb von Pipelines benötigen ggf. weitere Informationen. Gem. den 12 Factor Prinzipien kann man diese den Containern als Umgebungsvariablen bereitstellen. Gitlab selber hat eine ganze Reihe von [vordefinierten Umgebungsvariablen](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html), die man auswerten kann, um die Pipeline zu steuern. Z.B. könnte ein Build auf dem Master-Branch grundsätzlich in die Production Umgebung deployt werden, ein Build auf dem release Branch in die Staging Umgebung und alle anderen Branches nur in die Test Umgebung.
183

Nane Kratzke's avatar
Nane Kratzke committed
184
185
Sie können Informationen mittels Umgebungsvariablen im Wesentlichen auf die folgenden Arten an Jobs innerhalb von Pipelines übergeben:

Nane Kratzke's avatar
Nane Kratzke committed
186
1. Mittels Pipeline globaler Variablen indem Sie diese Variablen Toplevel in der `.gitlab-ci.yml` deklarieren. Also bspw. so:
Nane Kratzke's avatar
Nane Kratzke committed
187
188
189
190
191
192
193
194
195
   ```yaml
   variables:
      FOO: "BAR"
   ```
2. Mittels der bereits erwähnten [vordefinierten Umgebungsvariablen](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html), die vom Gitlab CI Build-System gesetzt werden.
3. Mittels in der Gitlab CI Oberfläche gesetzten Variablen. Diese können Sie in den [CI/CD Settings](../../../settings/ci_cd) (Variables) eines jeden Repositories setzen. Dies bietet sich insbesondere für Daten an, die niemals in eine Versionsverwaltung gehören - also insbesondere Zugangsdaten, Passwörter. Diese Variablen können in der Gitlab CI Oberfläche sogar als Masked gekennzeichnet werden, damit diese Werte nicht in Logdateien oder Konsolenausgaben im Klartext zu lesen sind.

Alle diese Variablen werden in Job Containern als Umgebungsvariablen gesetzt und können mittels der Standard Shell Variableninterpolation `$FOO` ausgewertet werden bzw. von Programmen ausgelesen werden. Häufig nutzt man dies dazu den Pipeline Prozess zu steuern. Dies soll dieses Beispiel veranschaulichen.

Nane Kratzke's avatar
Nane Kratzke committed
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
__Aufgabe:__

Ändern Sie die Pipeline nun bitte wie folgt ab:

```yaml
stages:
    - generate
    - consume

job1:
    stage: generate
    script:
        - mkdir build
        - echo "Hello I am job 1 executed on the $CI_COMMIT_REF_NAME branch only" > build/job1-result.txt
    artifacts:
        paths:
            - build/
    only:
        - master

job2:
    stage: generate
    script:
        - mkdir build
        - echo "Hello I am job 2 executed on the $CI_COMMIT_REF_NAME branch only" > build/job2-result.txt
    artifacts:
        paths:
            - build/
    only:
        variables:
            - $CI_COMMIT_REF_NAME == "release"

job3:
    stage: generate
    script:
        - mkdir build
        - echo "Hello I am job3 and always executed except for the master or release branch" > build/job3-result.txt
    artifacts:
        paths:
            - build/
    except:
        - master
        - release
            
job4:
    stage: consume
    script:
        - cat build/*-result.txt 
```

Nane Kratzke's avatar
Nane Kratzke committed
246
Dies erweitert die Pipeline um einen weiteren Job in der generate Stage. Jobs werden nun aber abhängig von Umgebungsvariablen ausgeführt.
Nane Kratzke's avatar
Nane Kratzke committed
247
248
249
250
251
252
253
254
255
256
257
258
259
260

- job1 wird nur auf dem master Branch ausgeführt.
- job2 wird nur auf dem release Branch ausgeführt.
- job3 wird auf allen anderen Branches ausgeführt.

Hierzu wurden `only` bzw. `except` den Jobs als Bedingung mitgegeben. 

- [`only`](https://docs.gitlab.com/ee/ci/yaml/#onlyexcept-basic) führt einen Job nur aus, wenn alle Bedingungen erfüllt sind.
- [`except`](https://docs.gitlab.com/ee/ci/yaml/#onlyexcept-basic) führt einen Job nur aus, wenn keine der Bedingungen erfüllt ist.

Veranschaulichen Sie sich die Wirkungsweise:

1. Pushen Sie dieses Pipeline einmal in den master Branch erhalten Sie die Ausgabe im job4
    ```
Nane Kratzke's avatar
Nane Kratzke committed
261
    Hello I am job 1, executed on the master branch only.
Nane Kratzke's avatar
Nane Kratzke committed
262
263
264
    ```
2. Pushen Sie diese Pipeline in den release Branch erhalten Sie die Ausgabe im job4.
    ```
Nane Kratzke's avatar
Nane Kratzke committed
265
    Hello I am job 1, executed on the release branch only.
Nane Kratzke's avatar
Nane Kratzke committed
266
267
268
    ```
3. Pushen Sie diese Pipeline in irgendeinen anderen Branch erhalten Sie die Ausgabe im job4.
    ```
Nane Kratzke's avatar
Nane Kratzke committed
269
    Hello I am job3, and always executed except for the master or release branch
Nane Kratzke's avatar
Nane Kratzke committed
270
271
    ```

Nane Kratzke's avatar
Nane Kratzke committed
272
Auf diese Weise lassen sich einzelne Jobs in der Pipeline nur unter Bedingungen ausführen, die sich über Umgebungsvariablen setzen lassen.
Nane Kratzke's avatar
Nane Kratzke committed
273

274
275
## Übung 4: Nutzung von Images

Nane Kratzke's avatar
Nane Kratzke committed
276
Bislang haben wir im Wesentlichen nur Kommandozeilen Programme des Linux/UNIX Standardumfangs genutzt. Wir wollen nun zwei kleine Hello-World Programme in Java und Python bauen, um zu demonstrieren, dass man in Jobs unterschiedliche Images für Jobs nutzen kann.
277
278
279

__Aufgaben:__

280
Sie finden hierzu im Ordner `src` die beiden folgenden Dateien (wenn nicht, legen Sie diese bitte mit den angegebenen Namen `Hello.java` und `hello.py` an):
281

282
__`Hello.java`:__
283
```java
284
285
286
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello " + args[0]);
287
    }
288
289
290
291
}
```

__`hello.py`:__
292
```python
293
294
295
import sys
print(f"Hello {sys.argv[1]}")
```
296
297
298

Passen Sie dann bitte Ihre `.gitlab-ci.yml`-Datei wie folgt an:

299
```yaml
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
variables:
    GREET: "Mundo"

stages:
    - test

java:
    stage: test
    script:
        - javac src/*.java
        - java -cp src/ Hello $GREET > result.txt
        - cat result.txt
        - cat result.txt | grep "Hello $GREET"

python:
    stage: test
    script:
        - python src/hello.py $GREET > result.txt
        - cat result.txt
        - cat result.txt | grep "Hello $GREET"
```

322
323
324
325
326
327
328
Wenn Sie diese Pipeline laufen lassen, werden Sie folgende Fehlermeldungen im Job *java* bzw. *python* bekommen:

```
javac: not found
python: not found
```

Nane Kratzke's avatar
Nane Kratzke committed
329
Dies hängt damit zusammen, dass das Standard Gitlab Job Image weder das Java JDK noch die Python Laufzeitumgebung beinhaltet. Sie können Jobs aber auf Basis anderer Images laufen lassen, z.B. mit den Standard Images, die auf DockerHub für die gebräuchlichsten Programmiersprachen angeboten werden. 
330
331

Sie können z.B.
332

333
334
- die Angaben `image: "openjdk:14"` im Job *java* hinzufügen, um vorzugeben, dass der *java* Job auf dem openjdk Image für Java 14
- und die Angabe `image: "python:3-slim"` im Job *python* hinzufügen, um vorzugeben, dass der *python* Job auf dem Python 3 slim (besonders kleines Python3 Image)
335

336
basieren soll.
337

338
Wenn Sie diese Angaben ergänzen, werden Sie sehen, dass die Pipeline nun durchläuft. Auf diese Weise können Sie also an unterschiedlichen Stellen in einer Pipeline unterschiedliche Container Images nutzen. Idealerweise sollten die Images natürlich kompatibel zur beabsichtigten Production Umgebung sein.
Nane Kratzke's avatar
Nane Kratzke committed
339

340
Wenn alle (oder viele) Jobs einer Pipeline auf demselben Image basieren sollen, können Sie diese `image` Angabe auch außerhalb der Jobs als Default Job Image der Pipeline angeben. Sie müssen dann nur noch bei den Jobs andere Images angeben, die explizit nicht mit dem Default Job Image der Pipeline laufen sollen.
Nane Kratzke's avatar
Nane Kratzke committed
341

Nane Kratzke's avatar
Nane Kratzke committed
342
## Was sollten Sie mitnehmen
Nane Kratzke's avatar
Nane Kratzke committed
343

Nane Kratzke's avatar
Nane Kratzke committed
344
345
346
347
348
349
350
351
352
353
354
355
1. Deployment Pipelines sind ein wesentlicher Baustein im DevOps Ansatz, um Entwicklungszyklen schnell und agil zu halten.
2. Gemäß dem Everything as Code Ansatz versucht man auch Deployment Pipelines als versionierbaren Code ausdrücken zu können.
3. Deployment Pipelines gliedern sich in eine Folge sequentiell ausgeführter Stages (o. ähnl. Bezeichnung).
4. Innerhalb von Stages laufen parallel ausführbare Jobs (o. ähnl. Bezeichnung).
5. Jeder Job läuft in einem Container, nicht alle Jobs bauen müssen dasselbe Container-Image haben.
6. Ein Job ist ein Shellskript (Sequenz von Commands), dass erfolgreich ist, wenn es den Exit Code 0 liefert. Wenn ein Command einen anderen Exit Code als 0 liefert, wird der Job als nicht erfolgreich abgebrochen.
7. Das Repository wird in jedem Job als Input bereitgestellt.
8. Ein Job kann Artefakte (Output) produzieren, die an Jobs in Folge-Stages weitergegeben und auch archiviert werden können.
9. Schlägt ein Job fehl, schlägt die Pipeline fehl.
10. Gitlab bietet mit `.gitlab-ci.yml` eine einfache Möglichkeit an, solche Deployment Pipelins als YAML-Dateien zu definieren und zu versionieren.

## Links
Nane Kratzke's avatar
Nane Kratzke committed
356

Nane Kratzke's avatar
Nane Kratzke committed
357
- Gitlab: [Job Artifacts](https://docs.gitlab.com/ee/ci/pipelines/job_artifacts.html)
358
- Gitlab: [Predefined Variables](https://docs.gitlab.com/ee/ci/variables/predefined_variables.html)
Nane Kratzke's avatar
Nane Kratzke committed
359
360
- Youtube: [Gitlab CI pipeline tutorial for beginners](https://youtu.be/Jav4vbUrqII)
- Youtube: [Gitlab CI Pipeline, Artifacts and Environments](https://youtu.be/PCKDICEe10s)