Neuigkeiten von trion.
Immer gut informiert.

Jenkins Buildslave für Docker

Bei der Nutzung von eigenen Docker-Images stellt sich früher oder später die Frage, wie eine sinnvolle Build-Automatisierung am besten umgesetzt wird. Der Docker-Host, auf dem die Images gebaut werden, läuft früher oder später in Probleme durch verwaiste Volumes, Image-Layer und ganze Images. Soll auch der Buildserver als Docker-Container betrieben werden, so wird die Situation noch etwas komplizierter: Auf keinen Fall soll versehentlich ein Container während des Builds oder sogar der Container des Buildservers selbst "aufgeräumt" werden.

Damit ergeben sich im Prinzip folgende Optionen

  • Sehr sorgfältig erstellte Clean-Up Jobs

  • Verwendung von Docker-in-Docker zur Isolation des Docker-Image Builds

  • Verwendung eines speziellen Build-Slave, der eine Docker Buildumgebung bereitstellt und jederzeit entsorgt und neu erstellt werden kann

Die zuletzt genannte Option wird im folgenden exemplarisch mit Jenkins, Ansible und Vagrant/VirtualBox vorgestellt. Vagrant dient dabei lediglich zur Veranschaulichung, in produktiven Umgebungen wird man auf Cloud-Resourcen oder VMware ESX zurückgreifen.

Docker in Docker vs. Phönix Maschinen

Um in einem unter Docker laufenden Buildsystem wiederum Docker zur Verfügung zu haben, gibt es prinzipiell die Möglichkeit, z.B. durch ein Socket-Mount, den Docker-Daemon des Hosts zu verwenden. Damit werden Umgebungen mit unterschiedlichen Anforderungen vermischt: Der Host benötigt für die Buildumgebung eine stabile Docker-Instanz, die lediglich ein einzelnes oder einige wenige Docker-Images beherbergt. Gleiches gilt für Docker-Volumes und gestartete Container. Veränderliche Daten beinhalten lediglich die Konfiguration des Buildsystems und unterstützende Dienste, jedoch keine temporären Images aus Build-Jobs.

Auch aus Sicherheitsgründen ist eine Trennung der Umgebungen ratsam: Aus einem Build sollte keine negative Beeinflussung des Hosts resultieren.

Als eine Form der Isolation könnte Docker-in-Docker eingesetzt werden. Dabei ergeben sich im Detail Probleme durch die geschachtelten Dateisysteme und im Zusammenhang mit der Security-Implementierung des Linux Kernels.

Damit bleibt als sinnvollste Option die Verwendung von Build-Slaves, die als sogenannte "Phönix Maschinen" so vorgesehen sind, dass sie regelmäßig zerstört und automatisiert neu augesetzt werden. Virtuelle Maschinen eigenen sich als Isolationslevel dazu sehr gut und lassen sich relativ einfach provisionieren und auch bedarfsgerecht skalieren.

Als Werkzeug zur Automatisierung von virtuellen Maschinen hat sich Vagrant in Entwicklerkreisen etabliert. Stellvertretend für andere Provisionierungsansätze wird daher Vagrant im folgenden verwendet.

Jenkins Slave Launcher

Im weiteren wird davon ausgegangen, dass Jenkins als Docker-Container betrieben wird. Als Docker-Netzwerk wird das Bridge-Netzwerk 172.17.0.0/16 mit dem Docker-Host unter 172.17.0.1. (Die Netzwerkkonfiguration ist in diesem Beispiel lediglich durch die Konstellation mit Vagrant relevant.)

Jenkins als Docker-Container
mkdir jenkins-data

docker run -it -u $(id -u) --rm \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins-data:/var/jenkins_home \
--name jenkins \
jenkins/jenkins:alpine

Jenkins erlaubt auf einfache Weise Build-Slaves einzurichten.

Dazu wird unter Manage Jenkins >> Manage Nodes der Eintrag "New Node" ausgewählt. Um Jenkins einen Slave bekannt zu machen, kann Jenkins aktiv einen entsprechenden Build-Slave via SSH provisionieren oder alternativ eine Java Webstart Konfiguration bereitstellen, wodurch sich der Client mit dem Jenkins Knoten verbindet.

Zur Provisionierung via SSH wird zunächst ein Name für den Jenkins-Slave vergeben, zum Beispiel "docker slave" und als Host "172.17.0.1" konfiguriert. Damit Jenkins sich via SSH auf den Docker Build-Slave verbinden kann, werden noch Zugangsdaten benötigt. Im einfachsten Fall können diese Credentials Username und Passwort sein, zum Beispiel "jenkins" und "password". Da der SSH Port unter 2201 zur Verfügung stehen wird, muss dieser unter "advanved" noch eingetragen werden.

Als nächstes wird die Bereitstellung der virtuellen Maschine durch Vagrant vorgenommen. Vagrant nutzt sogenannte Basis-Images für die zu erzeugenden virtuellen Maschinen. Damit Docker lauffähig ist, wird ein 64bit Linux-System verwendet, im Beispiel ein Ubuntu.

Zunächst werden Einstellungen der virtuellen Maschine, wie das zur Verfügung stehende RAM und das Port-Forwarding von Port 2201 auf dem Host zu dem durch SSH genutzten Port 22 des Gastsystems angegeben. Anschließend folgt die Konfiguration des Systems innerhalb der virtuellen Maschine.

Vagrant unterstützt mehrere Konfigurationswerkzeuge, um die virtuelle Maschine einsatzbereit zu machen. Für die Konfiguration wird Ansible verwendet, doch zunächst muss sichergestellt werden, dass eine lauffähige Python-Umgebung für Ansible bereit steht - dies erledigt ein einfaches Shellkommando. Anschließend kann Ansible durch Vagrant aufgerufen werden. (Damit Ansible genutzt werden kann, muss dies auf dem Rechner, auf dem Vagrant ausgeführt wird, installiert werden.)

Das vollständige Vagrantfile ist im folgenden zu sehen:

Vagrantfile
require 'socket'

#local machine name as instance identifier
host = "jenkins-#{Socket.gethostname}"

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/zesty64"
  config.vm.box_check_update = false

  config.vm.hostname = host
  config.vm.provider "virtualbox" do |vb|
    vb.memory = "2560"
    vb.cpus = "2"
    vb.name = "jenkins-slave"
  end
  config.vm.network "forwarded_port", guest: 5005, host: 5005
  #ssh for jenkins
  config.vm.network "forwarded_port", guest: 22, host: 2201


  config.vm.provision "shell", inline: <<-SHELL
    sudo apt-get install python-minimal -y
  SHELL

  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "playbook.yml"
  end

end

Ansible installiert die von Jenkins benötigte Java-Laufzeitumgebung und richtet den Nutzer ein, mit dem sich der Jenkins-Master anmeldet, um die Jenkins-Slave Konfiguration vorzunehmen. Außerdem wird Docker eingerichtet - schließlich soll der Jenkins-Slave Docker-Images erzeugen. Die Konfiguration für Ansible könnte beispielsweise wie folgt aussehen:

Ansible playbook.yml
---
- hosts: all
  become_method: sudo
  tasks:
    - name: install Java 8 runtime
      become: yes
      apt:
        name: openjdk-8-jre-headless
        state: present

    - name: install socat util
      become: yes
      apt:
        name: socat
        state: present

    - name: ensure docker repository key is installed
      become: yes
      apt_key:
        id: "9DC858229FC7DD38854AE2D88D81803C0EBFCD88"
        keyserver: "hkp://p80.pool.sks-keyservers.net:80"
        state: present

    - name: ensure docker package repository is available
      become: yes
      apt_repository:
        repo: 'deb [arch=amd64] https://download.docker.com/linux/ubuntu xenial stable'
        state: present

    - name: ensure docker and dependencies are installed
      become: yes
      apt:
        name: docker-ce
        update_cache: yes

    - name: ensure docker can use insecure registries in 10/8
      become: yes
      lineinfile: "dest=/etc/default/docker regexp=^DOCKER_OPTS line=DOCKER_OPTS='--insecure-registry 10.0.0.0/8'"

    - name: restart docker service
      become: yes
      service:
        name: docker
        state: restarted

    - name: add jenkins group
      become: yes
      group:
        name: jenkins
        state: present

    - name: add jenkins user
      become: yes
      user:
        password: "{{ 'password' | password_hash('sha512') }}"
        update_password: on_create
        name: jenkins
        comment: "Jenkis User"
        uid: 1001
        group: jenkins
        groups: docker

Die Konfiguration ist nun abgeschlossen und die virtuelle Maschine kann mittels vagrant up gestartet werden.

Jenkins kann den Build-Slave nun in Betrieb nehmen. Damit ausschließlich Jobs auf diesem - und möglicherweise weiteren Slaves - ausgeführt werden, jedoch nicht auf dem Master, sollte dieser als Ziel für Build-Jobs deaktiviert werden. Dies erfolgt ebenfalls unter der Node-Verwaltung von Jenkins.

Mit diesem Setup auf Basis von temporären virtuellen Maschinen lassen sich alle Anforderungen an Skalierbarkeit, Sicherheit und Isolation erfüllen. Die Verwendung von Phönix-Servern als Build-Slaves bringt somit deutliche Vorteile und sollte als best-practice etabliert werden.




Zu den Themen Ansible, Jenkins und Docker bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Zur Desktop Version des Artikels