Automatisiert Docker Images zu bauen ist sehr nützlich und sinnvoll um den Build-Prozess zu automatisieren und automatisch jeden Tag, bei jedem neuen Tag oder zum Testen neue Images zu bauen und automatisiert den Nutzern oder der eigenen Infrastruktur zur Verfügung zu stellen. Wir wollen hier die Images bauen und richtig taggen in Abhängigkeit davon ob es ein Tag push oder ein master push ist.
Bei einem Push zu dem Master Branch wird ein Image mit dem Tag staging
angelegt um sicherzustellen, dass latest nur aus produktiven Quellen kommt.
Bei einem Push mit einem Tag werden folgende Images gebaut
1IMAGE_REGISTRY:v0.0.0
2IMAGE_REGISTRY:v0.0
3IMAGE_REGISTRY:v0
4IMAGE_REGISTRY:latest
Hiermit kann man in jeder Stufe des Updates die Tags referenzieren und in der ausrollung aussuchen ob man das Major-Update oder Minor-Updates automatisch mitausrollt. Wir legen außerdem noch ein latest Image an, da es zum einen der Standard ist so ein Tag zu haben und zum anderen uns auch die Möglichkeit gibt immer das aktuellste zum laufen zu bringen.
Um ein Docker Image in einer Pipeline zu bauen müssen wir zuerst ein funktionierendes Dockerfile
in unser Repository hinzufügen. Für unser Beispiel nehmen wir mal ein sehr einfaches Image mit nur einem Base-Image.
1FROM ubuntu
2
Inhalt des Dockerfile
Als nächstes müssen wir unsere .gitlab-ci.yml
Datei wieder hinzufügen. Außerdem fügen zwei Stages ein (für Release und zum bauen). Nun könen wir erstmal als default ein docker Image angeben, da wir somit Zugriff auf das docker-Binary bekommen um das Image zu bauen. Als Service kommt ebenfalls ein docker Image mit dem dind (docker in docker) um auch den daemon laufen zu haben. Um uns in unserer Gitlab-Registry auch anzumelden benutzen wir ein Script, welches immer Vor den einzelnen Jobs läuft. Hier kann natürlich auch eine Anmeldung an eine andere Registry eingebunden werden. Unserer kompletter default
Bereich sieht nun so aus:
1default:
2 image: docker:20.10.16
3 services:
4 - docker:20.10.16-dind
5 before_script:
6 - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
Als stages benutzen wir eine Build-stage wo das Image gebaut wird und eine Release-stage wo wir das Image entsprechend Taggen und nochmal pushen werden.
1stages:
2 - build
3 - release
Für die Variablen benötigen wir einmal unseren Docker Host aus dem Service, den Pfad wo die Zertifikate liegen und wir benutzen den Namen für unsere Test-Images und unsere Release Images um Sie auch global anlegen zu können.
1variables:
2 DOCKER_HOST: tcp://docker:2376
3 DOCKER_TLS_CERTDIR: "/certs"
4 CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:staging
5 CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
Wir definieren nun unseren ersten Job in der build pipeline. Hierfür vergeben wir auch wieder einen Namen für den Job, der später in der UI und in den Logs referenziert wird und weisen den job der Stage build
zu. Mit dem Schlüsselwort only
können wir festlegen für was der Job ausgeführt wird. Hier kann einfach der Branchname also master
reingeschrieben werden. Das script besteht nun aus einem docker build mit dem Namen aus der Variablen für Test Images. Zu empfehlen ist noch der Parameter --pull
um im Dockerfile referenzierte Images auch in der neusten Version zu nutzen und nicht ggf. gecachte Images zu nehmen.
1build_master:
2 stage: build
3 script:
4 - docker build --pull -t $CONTAINER_TEST_IMAGE .
5 - docker push $CONTAINER_TEST_IMAGE
6 only:
7 - master
Da wir für den master-Branch nichts weiter machen möchten können wir uns nun an die definition für die Produktions Pushes begeben.
Normalerweise wird in der produktion mit Tags gearbeitet. Es kann natürlich aber auch noch eine weitere pipeline angelegt werden, die genauso aussieht wie die vom master, nur mit dem branch auf produktion. Das wollen wir hier aber nicht machen, sondern uns nun um die semver-Versionierten Images kümmern.
Im ersten Schritt suchen wir uns wieder einen Namen aus und weisen es der Stage build
zu. Als Wert für den Parameter only
nehmen wir nun aber tags
um die Jobs auf tags zu beschränken. Das Script sieht nun wieder genauso aus wie das vom Master, nur nehmen wir als Tagname natürlich die Release Variable.
1build_tag:
2 stage: build
3 script:
4 - docker build --pull -t $CONTAINER_RELEASE_IMAGE .
5 - docker push $CONTAINER_RELEASE_IMAGE
6 only:
7 - tags
Für die erweiterten Tags erstellen wir uns einen neuen Job, der diesmal in die Release Stage kommt und somit nach unserer Build Pipeline ausgeführt wird. Das machen wir, damit wir das Image aus der Build-Pipeline weiter benutzen können und nur neue Tags vergeben. Anfangen tun wir also mit dem pullen unserer Release Images. Mit dem Befehl cut
können wir nun die Variable die unser Tag enthält zerschneiden. Mittels cut -d.
zerschneiden wir den String an Punkten. Mittels -f1 kriegen wir den ersten Zerschnittenen Teil zurück. So können wir unsere Tags dann wie folgt aufbauen.
1release-image:
2 stage: release
3 script:
4 - docker pull $CONTAINER_RELEASE_IMAGE
5 - docker tag $CONTAINER_RELEASE_IMAGE $CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | cut -d. -f1)
6 - docker tag $CONTAINER_RELEASE_IMAGE $CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | cut -d. -f1).$(echo $CI_COMMIT_TAG | cut -d. -f2)
7 - docker tag $CONTAINER_RELEASE_IMAGE $CI_REGISTRY_IMAGE:${CI_COMMIT_TAG}
8 - docker push $CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | cut -d. -f1)
9 - docker push $CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | cut -d. -f1).$(echo $CI_COMMIT_TAG | cut -d. -f2)
10 - docker push $CI_REGISTRY_IMAGE:${CI_COMMIT_TAG}
11 only:
12 - tags
1default:
2 image: docker:20.10.16
3 services:
4 - docker:20.10.16-dind
5 before_script:
6 - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
7
8stages:
9 - build
10 - release
11
12variables:
13 DOCKER_HOST: tcp://docker:2376
14 DOCKER_TLS_CERTDIR: "/certs"
15 CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:staging
16 CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest
17
18build_master:
19 stage: build
20 script:
21 - docker build --pull -t $CONTAINER_TEST_IMAGE .
22 - docker push $CONTAINER_TEST_IMAGE
23 only:
24 - master
25
26build_tag:
27 stage: build
28 script:
29 - docker build --pull -t $CONTAINER_RELEASE_IMAGE .
30 - docker push $CONTAINER_RELEASE_IMAGE
31 only:
32 - tags
33
34release-image:
35 stage: release
36 script:
37 - docker pull $CONTAINER_RELEASE_IMAGE
38 - docker tag $CONTAINER_RELEASE_IMAGE $CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | cut -d. -f1)
39 - docker tag $CONTAINER_RELEASE_IMAGE $CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | cut -d. -f1).$(echo $CI_COMMIT_TAG | cut -d. -f2)
40 - docker tag $CONTAINER_RELEASE_IMAGE $CI_REGISTRY_IMAGE:${CI_COMMIT_TAG}
41 - docker push $CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | cut -d. -f1)
42 - docker push $CI_REGISTRY_IMAGE:$(echo $CI_COMMIT_TAG | cut -d. -f1).$(echo $CI_COMMIT_TAG | cut -d. -f2)
43 - docker push $CI_REGISTRY_IMAGE:${CI_COMMIT_TAG}
44 only:
45 - tags
Die Registry sieht nach einem push von einem Tag und einem Master nun wie folgt aus: