de DevOps Software Gitlab Pipelines

Ein Docker Image in Gitlab bauen pushen

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.

Push zu Master

Bei einem Push zu dem Master Branch wird ein Image mit dem Tag staging angelegt um sicherzustellen, dass latest nur aus produktiven Quellen kommt.

Push mit Tag

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.

Das Dockerfile

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

Die Pipeline-Datei

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

Die Definition - Master

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.

Die Definition - Tags

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

Die Fertige Pipeline-Datei

 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

Fertige Images

Die Registry sieht nach einem push von einem Tag und einem Master nun wie folgt aus: Images wurden alle gebaut und getagged