Jenkins und die Dockerception

Nach meinen ersten Experimenten mit Jenkins und Docker geht es nun wieder einen Schritt weiter. Mein Jenkins läuft im Docker-Container hinter einem NGINX im Docker-Container. Was könnte also noch eine Steigerung sein? Noch schöner wäre es wenn die Tests bzw Builds in eigenen Containern laufen, die dann nach dem Run wieder in sich zusammen fallen. Ich versuche mal alles zusammen zuschreiben.

Docker in Docker

Der beste Weg ist es den Docker-Socket als Volume an den Container durchzureichen. Hier der docker-compose-Eintrag:

jenkins:
  container_name: jenkins
  build: ./jenkins/
  volumes:
    - "/srv/www/jenkins:/var/jenkins_home"
    - "/usr/bin/docker:/bin/docker"
    - "/usr/lib/x86_64-linux-gnu/libapparmor.so.1.1.0:/usr/lib/x86_64-linux-gnu/libapparmor.so.1"
    - "/var/run/docker.sock:/var/run/docker.sock"
  ports:
    - "8080:8080"
    - "50000:50000"

Wir müssen nicht nur den Socket durchreichen, sondern auch das Docker-Binary und eine Library (war in diesem Fall nötig). Sonst nichts besonderes hier. Die Ports sind einmal für die Jenkins Weboberfläche und einmal für die Kommunikation mit den Slaves die sich im Netz befinden. In meinem Fall ein Windows Build Slave.

Docker in Jenkins

Ich benutze das offizielle Jenkins Docker Image mit ein paar Anpassungen. Das Dockerfile sieht wie folgt aus:

FROM jenkins

USER root

RUN apt-get update \
 && apt-get install -y \
    rsync \
 && rm -rf /var/lib/apt/lists/*

RUN addgroup --gid 116 docker \
 && usermod -a -G docker jenkins

USER jenkins

Ich installiere ein rsync und füge den Benutzer jenkins der docker-Gruppe hinzu damit der die Container starten, beenden und löschen kann.

Jenkins hat viele Plugins. Diese wiederum haben kaum bis garkeine Dokumentation oder klaffende Lücken in ihr. Also habe ich erstmal, ziemlich naiv, das Docker Plugin installiert. Jenkins braucht erstmal eine Connection zu Docker. Dazu gehen wie in die Jenkins System Konfiguration in die Cloud Sektion. Neben einem Namen sollten wir auch die Docker URL eintragen. Die kann auch ein Socket sein. In diesem Fall unix:///var/run/docker.sock. Nun klappt die Kommunikation.

tox-jenkins-slave

Nun brauchen wir ein Docker-Image das als Jenkins-Slave taugt. Ich habe mich ganz an dem Image von evarga orientiert. Mein Image beinhaltet ein paar Änderungen. Zum Beispiel installiere ich nicht das gesammte JDK, sondern nur ein Headless-JRE. Sonst installiert er mir schön diverse X-Libraries. Die braucht man nicht um den Jenkins-Slave-Client auszuführen. Ich bin riesen tox-Fan. Ein Tool um Tests in diversen Python Versionen auszuführen. Alles getrennt durch Virtualenv's. Dazu braucht tox aber auch die Python Versionen installiert. Dies funktioniert dank dem PPA von fkrull ganz wunderbar. Zum Schluß installiere ich tox selber und fertig ist das Image. Ach ja, ein openssh-server und git wird auch noch gebraucht. Hier das Dockerfile:

FROM ubuntu:trusty

RUN gpg --keyserver keyserver.ubuntu.com --recv-keys DB82666C \
 && gpg --export DB82666C | apt-key add -

RUN echo deb http://ppa.launchpad.net/fkrull/deadsnakes/ubuntu trusty main >> /etc/apt/sources.list \
 && echo deb-src http://ppa.launchpad.net/fkrull/deadsnakes/ubuntu trusty main >> /etc/apt/sources.list

RUN apt-get update \
 && apt-get install -y \
    git \
    openssh-server \
    openjdk-7-jre-headless \
    python-pip \
    python2.3 \
    python2.4 \
    python2.5 \
    python2.6 \
    python3.1 \
    python3.2 \
    python3.3 \
    python3.4 \
    python3.5 \
 && rm -rf /var/lib/apt/lists/*

RUN pip install tox

RUN sed -i 's|session    required     pam_loginuid.so|session    optional     pam_loginuid.so|g' /etc/pam.d/sshd \
 && mkdir -p /var/run/sshd

RUN adduser --quiet jenkins \
 && echo "jenkins:jenkins" | chpasswd

EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]

Es wird ein jenkins Benutzer mit dem Password jenkins angelegt. Wenn sich jemand daran stört soll er sich das Image selber bauen und dies verändern.

Um es zum Einsatz zu bringen müssen wir unter Configure System -> Cloud -> Docker -> Add Docker Template folgendes einstellen:

  1. Docker Image: xsteadfastx/tox-jenkins-slave
  2. Remote Filing System Root: /home/jenkins
  3. Labels: tox-jenkins-slave
  4. Launch method: Docker SSH computer launcher
  5. Credentials: Add a new one with username jenkins and password jenkins
  6. Remote FS Root Mapping: /home/jenkins
  7. Remove volumes: True

Mir war nicht klar das das Label ausschlaggebend ist um den Job auch wirklich auf den passenden Container laufen zu lassen. Ohne Label kann dieser nicht getriggert werden.

Nun können wir im Job den Container einsetzen:

  1. Docker Container: True
  2. Restrict where this project can be run: True
  3. Label Expression: tox-jenkins-slave

Sachen mit denen ich noch so gekämpft habe

Aus irgendeinem Grund hatte ich riesige Probleme mit den Locales in dem Container. Einige Tests haben Vergleichsdaten in Textfiles gespeichert. Falsche Locales haben die Tests gebrochen. Das Absurde... egal was ich tat, es half einfach nichts. LANG setzen in der .bashrc oder in /etc/profile wurde vollständig ignoriert. Startete ich den Container manuell und loggte mich per SSH ein, kein Problem, die Tests liefen durch. Der Slave gestartet von Jenkins, völlige Ingoranz meiner Config. Also musste ich es direkt in das Python Job Script packen:

import os
import tox

os .environ['LANG'] = 'C.UTF-8'
os.chdir(os.environ['WORKSPACE'])

tox.cmdline()

Ich setze die Environment-Variabel direkt im Script, gehe in den Jenkins-Workspace und führe tox aus. Ein Umweg, aber es funktioniert.

Schenkt den PyPI-Servern eine Auszeit

Noch eine Kleinigkeit: Da die Abhängigkeiten für die Tests bei jedem Durchlauf neu heruntergeladen werden lohnt sich der Einsatz von devpi. Der kann sehr viel mehr, wird aber von mir als einfacher PyPI-Mirror missbraucht. Ich habe dafür auch ein kleines Image. Läuft der Container sieht mein Jenkins Job kompletto so aus:

import os
import tox

os.environ['LANG'] = 'C.UTF-8'

os.makedirs('/home/jenkins/.config/pip')
with open('/home/jenkins/.config/pip/pip.conf', 'w') as f:
  f.write('[install]\ntrusted-host = 192.168.1.8')

os.environ['PIP_INDEX_URL'] = 'http://192.168.1.8:3141/root/pypi/+simple/'

os.chdir(os.environ['WORKSPACE'])

tox.cmdline(['-e py33,py34,py35'])

Von nun an wird der Mirror benutzt.

source: http://www.tumblr.com

Jenkins, Docker und Ansible

Und ab in die Docker Hölle. Irgendwie scheint mir Docker nicht zu liegen. Es fängt schon damit an das mir noch nicht so ganz bewusst ist wieso man nicht einfach den Container schön macht und ihn dann immer wieder mit docker start mein-toller-container startet. Ich glaube es geht darum alles so weit wie möglich unabhängig zu machen. So mehr ich es benutze um so mehr wird mir langsam klar wie ich es "richtig" benutzen kann. Es scheint aber noch ein sehr langer und schmerzhafter Weg zu werden.

Da ich in letzter Zeit ein wenig mit ansible rumgespielt habe, dachte ich ob dies ein Weg wäre alles für mich zu vereinfachen. Eine andere Alternative wäre Docker Compose gewesen. Es geht immer darum seine Container in einem YAML-File zu definieren und anstatt einer Batterie an Kommandos einzugeben, einfach den Composer oder halt Ansible anzuschmeißen. Docker kann schließlich schon ziemlich komplex werden wenn man Data-Container benutzt und auch noch mehrere Container miteinander verlinkt. Ansible nimmt einem dabei nicht nur das Orchestrieren mit Docker ab, es kümmert sich auch gleich noch um das gesammte Setup.

Ich habe mir mal ein praktisches Beispiel genommen. Ich habe ja auf meinem Raspi ein Jenkins laufen. Nicht gerade ein mächtiges Arbeitstier. Also wollte ich Jenkins schon seit einer Weile umziehen und dabei den Server nicht voll müllen. Schließlich braucht Jenkins für meine Tests auch Test-Datenbank usw. Diese sollten sich am besten nicht mit dem Host-System mischen. Dies ist das Ansible-Playbook was ich dafür genutzt habe. Ich versuche die Schritte durch die Kommentare zu erläutern. Das ganze gibt es auch als Repo auf GitHub.

---
# In diesem Fall führe ich alles lokal aus. Dies funktioniert natürlich auch
# perfekt remote.
- hosts: localhost

  # Eine Ubuntu-Box hier. Also wäre sudo nicht schlecht.
  sudo: yes

  vars:
    # Die user-Variabel benutze ich vor allem um meine Images zu benennen.
    - user: xsteadfastx

    # Es wird ein lokales temp-Verzeichnis definiert. Dies wird gebraucht
    # um die Docker Images zu bauen.
    - temp_docker_build_dir: /tmp/docker-jenkins

    # Das offizielle MySQL Image braucht ein definiertes root-Password um
    # zu bauen.
    - mysql_root_password: nicepassword

  tasks:

    # Wir adden den Docker GPG-Key damit wir die offiziellen Pakete ziehen
    # können. Ich habe die ID aus dem Install-Script rausgepoppelt.
    # Keine Garantie ob er in Zukunft auch stimmt.
    - name: APT | add docker repo key
      apt_key: keyserver=hkp://p80.pool.sks-keyservers.net:80
               id=36A1D7869245C8950F966E92D8576A8BA88D21E9
               state=present

    # Wir adden das offizielle Docker Repo für frische Versionen.
    - name: APT | add docker repo
      apt_repository: repo="deb https://get.docker.com/ubuntu docker main"
                      update_cache=yes
                      state=present

    # Docker installieren.
    - name: APT | install docker
      apt: name=lxc-docker
           update_cache=yes
           cache_valid_time=600
           state=present

    # Auf dem Docker-Host benötigt Ansible Python-Bindings. Das Problem mit
    # mit den offiziellen "python-docker" debs: Sie waren schlicht und einfach
    # zu alt. Also müssen wir es von PyPi installieren. Eigentlich nicht die
    # tolle Art... aber ich mache mal eine Ausnahme.
    - name: PIP | install docker-py
      pip: name=docker-py
           version=1.1.0

    # Es soll ein Temp-Verzeichnis zum bauen des jenkins-data Images
    # angelegt werden.
    - name: DIR | create jenkins-data docker build directory
      file: path="{{ temp_docker_build_dir }}/jenkins-data"
            state=directory

    # Kopiere das Dockerfile in des temporäre Build-Verzeichnis
    - name: COPY | transfer jenkins-data Dockerfile
      copy: src=files/Dockerfile-jenkins-data
            dest="{{ temp_docker_build_dir }}/jenkins-data/Dockerfile"

    # Endlich kann es losgehen... und wir können das Image bauen.
    - name: DOCKER | build jenkins data image
      docker_image: path="{{ temp_docker_build_dir }}/jenkins-data"
                    name="{{ user }}/jenkins-data"
                    state=present

    # Das gleiche machen wir jetzt für unser leicht angepasstes Dockerfile.
    - name: DIR | create jenkins docker build directory
      file: path="{{ temp_docker_build_dir }}/jenkins"
            state=directory

    - name: COPY | transfer jenkins Dockerfile
      copy: src=files/Dockerfile-jenkins
            dest="{{ temp_docker_build_dir }}/jenkins/Dockerfile"

    - name: DOCKER | build jenkins image
      docker_image: path="{{ temp_docker_build_dir }}/jenkins"
                    name="{{ user }}/jenkins"
                    state=present

    # Nun löschen wir das temporäre Verzeichnis.
    - name: RM | remove temp docker build directory
      file: path="{{ temp_docker_build_dir }}"
            state=absent

    # Das interessante an data-only Volumes ist, dass sie nicht gestartet
    # werden müssen um benutzt zu werden. Sie sind nur dazu da um von anderen
    # Containern genutzt zu werden. Es werden Volumes definiert die wir dann
    # benutzen zu können. Auch wenn der Jenkins Container gelöscht und wieder
    # neugestartet wird... benutzt man diesen Data-Container bleiben die Daten
    # in "/var/jenkins_home" bestehen.
    - name: DOCKER | jenkins data volume
      docker:
        name: jenkins-data
        image: "{{ user }}/jenkins-data"
        state: present
        volumes:
          - /var/jenkins_home

    # Das gleiche machen wir für den mysql-data Container.
    - name: DOCKER | mysql data volume
      docker:
        name: mysql-data
        image: busybox
        state: present
        volumes:
          - /var/lib/mysql

    # Nun starten wir den mysql Container. Wir benutzen das offizielle
    # MySQL-Image. Der state "reloaded" bedeutet, dass wenn sie die Config
    # ändert... dann wird der Container entfernt und neu erstellt.
    # Wichtig ist hier das "volumes_from". Hier geben wir den Container an
    # von dem die definierten Volumes benutzt werden sollen. Also werden sie
    # in einem eigenes dafür angelegten Container gespeichert.
    # Kapselung und so. Mit "env" können wir auch noch diverese
    # Environment dem Container mitgeben. In diesem Fall ein Passwort für den
    # root-User für den Server. Diese Variabel wird gebraucht damit der
    # erstellte Container auch gestartet werden kann.
    - name: DOCKER | mysql container
      docker:
        name: mysql
        image: mysql:5.5
        state: reloaded
        pull: always
        volumes_from:
          - mysql-data
        env:
          MYSQL_ROOT_PASSWORD: "{{ mysql_root_password }}"

    # now finally the jenkins container. it links to the mysql container
    # because i need some mysql for some tests im running in jenkins.
    # and with the link i can easily use the environment variables in jenkins.
    # i bind the jenkins port only to localhost port 9090. i use nginx for
    # proxying. in "volumes" i define my host directory in which my git repos
    # are. so jenkins think its all in a local directory.
    # and "/var/jenkings_home" is stored in the "jenkins-data" container.
    # voila.

    # Hier ist nun das Herzstück. Hier kommt alles zusammen. Wir erstellen
    # den Jenkins Container und starten ihn. Wir benutzen als Basis unser
    # Jenkins Image. Mit "links" können wir mehrere laufende Container
    # miteinander verbinden. Diese sind dann untereinander über ein virtualles
    # Netz verbunden. Gleichzeitig stellt Docker dann Environment-Variables
    # zu verfügung. Diese können wir ganz prima im System nutzen. In diesem
    # Beispiel brauche ich für Tests in Jenkins ein paar MySQL-Datenbanken.
    # So ist der MySQL-Container im Jenkins-Container sichtbar und vor allem
    # benutzbar. Mit "ports" können wir bestimmte Ports nach Aussen mappen.
    # In diesem Fall mappe ich den standard Jenkins Port "8080" auf localhost
    # Port "9090". Perfekt um ihn per NGINX von Aussen zugänglich zu machen.
    # Mit "volumes" definieren wir lokale Verzeichnisse die an einer
    # bestimmten Stelle im Container zu verfügung gestellt werden. In diesem
    # Fall den lokalen Ordner mit den Git-Repos "/srv/git" in das
    # Container-Filesystem unter "/data". Also kein gefummel mit SSH-Keys
    # um Git im Container zu benutzen. Als letztes "volumes_from".
    # Damit können wir die Volumes aus einem anderen Container, in diesem Fall
    # des data-only Containers "jenkins-data", benutzen. Diese Daten würden
    # so den Tot des Jenkins Container überleben. Genau das was wir wollen.
    - name: DOCKER | jenkins container
      docker:
        name: jenkins
        image: "{{ user }}/jenkins"
        state: reloaded
        links:
          - "mysql:mysql"
        ports:
          - "127.0.0.1:9090:8080"
        volumes:
          - "/srv/git:/data"
        volumes_from:
          - jenkins-data

Hier das jenkins-data Dockerfile:

FROM busybox

RUN mkdir -p /var/jenkins_home \
  && chown -R default /var/jenkins_home

VOLUME /var/jenkins_home

CMD ["true"]

Und hier mein Jenkins Dockerfile. Ich benutze eigentlich das Offizielle noch mit ein paar extra Sachen die ich installiere. Dies sind auch spezielle Packages für meine Tests. Also Python-Kram.

FROM jenkins

USER root

RUN apt-get update && apt-get install -y python-dev python-setuptools python-virtualenv python-pip libjpeg-dev ansible mysql-client && rm -rf /var/lib/apt/lists/* && pip install tox

Also schießen wir mal los:

ansible-playbooks -i hosts site.yml -c local --ask-become-passansible-playbooks -i hosts site.yml -c local --ask-become-pass

Na mal schauen ob irgendwann der Zeitpunkt kommt an dem ich Docker besser verstehe. Ich verlinke hier nochmal den Docker Beitrag von Andrew T. Baker von der PyCon 2015.

Jenkins, ssh und Probleme

Mein im vergangenes Post beschriebens Problem war dann doch nicht so einfach zu lösen. Nun ein neuer Ansatz: Den Dump einfach per Rsync ziehen. Also schön ssh-Keys für den Jenkins-User angelegt und gehofft:

jenkins@box:~$ ssh userfoo@hostname
Host key verification failed.

Ah ja. Makes sense. Irgendwie war der User nicht in der Lage das known_hosts-File anzulegen. Habe wirklich viel dran rumprobiert bis ich auf den Workaround kam:

Man loggt sich auf dem Jenkins-Server und auf der Bash ein mit

sudo su jenkins -s /bin/bash

Dann einfach mal ein

jenkins@box:~$ ssh userfoo@hostname

Und dann legt er auch known_hosts an und der Job läuft auf einmal im Jenkins. Was auch immer...