Heutzutage schreiben immer noch viele Entwickler Dockerfiles mit der Hand und respektieren dabei vielleicht sogar bestimmte Best Practices oder Ratschläge zur Lösung von Problemen, wie z.B. das PID-1-Problem. Es scheint, als würde die Geschichte sich wiederholen: In der Vergangenheit hat jeder in Handarbeit Server und virtuelle Maschinen hergestellt. Und jetzt machen wir das Gleiche mit Containern.
Abgesehen von diesen Laufzeitproblemen ist die Handhabung von Dockerfiles wirklich albern. Warum nicht mit Provisionierungswerkzeugen? Hier kommt also der Packer von HashiCorp ins Spiel.
Inzwischen weiß jeder, was Docker ist: alles wird in Container verpackt. Immer mehr Tools tauchen auf, um Probleme zu lösen, die sich aus diesen Änderungen ergeben.
Schauen wir uns die Technologien an, die man heutzutage häufig zum Bauen von Docker-Images verwendet:
Packer
- Der Packer wird verwendet, um ein Image aus einem Base Image zu erstellen, grundlegende Einstellungen durchzuführen und das endgültige Image zu speichern (Commit)
- Wir nutzen Provisioner und Packer Templates, um die eigentliche Arbeit zur Erstellung des endgültigen Images zu erledigen
- Ansible wird zur Provisioning genutzt
Ansible
- Ansible führt Playbooks auf localhost (im Docker-Container) aus.
- Ansible muss auf dem Image installiert sein, das die Basis für das eigene Image wird
- Packer lädt den Inhalt des Ansible Playbooks in die Docker-Container-Instanz hoch und führt es lokal aus
Normalerweise konfigurieren wir Docker-Images, indem wir mehrere RUN-Befehle hinzufügen, z.B.
# Install PanDoc RUN yum -y install epel-release && yum -y install pandoc # chrome and xvfb RUN cd /tmp/setup && \ curl https://intoli.com/install-google-chrome.sh | bash && \ yum install -y xorg-x11-server-Xvfb # Install oc and jq RUN yum -y install epel-release && yum -y install jq # Install headless Java RUN yum install -y centos-release-scl-rh && \ INSTALL_PKGS="git java-1.8.0-openjdk-headless rsync" && \ yum -y --setopt=tsflags=nodocs install $INSTALL_PKGS && \ mkdir -p /home/jenkins && mkdir -p /var/lib/origin \ chown -R 1001:0 /home/jenkins && \ ... unlink /usr/bin/java && \ ...
Anstelle einer riesigen Verkettung von RUN-Befehlen können wir mit Ansible Tasks und Rollen ausführen, um das Docker-Image zu konfigurieren. Um direkt einen Schritt weiter zu gehen, verwenden wir den Ansible Provisioner in Packer. Wir benötigen dazu einige Dateien als Grundgerüst:
- build.json
- ansible.cfg
- ansible/playbook
Die build.json besteht dabei aus drei Hauptbestandteilen:
{ "builders": [{ "type": "docker", "image": "jenkinsci/ssh-slave:latest", "commit": true, "changes": [ "VOLUME /data", "WORKDIR /data", "EXPOSE 6379", "ENTRYPOINT [\"docker-entrypoint.sh\"]", "CMD [\"redis-server\"]" ] }], "provisioners":[{ "type": "ansible", "user": "root", "playbook_file": "ansible/playbook.yml", "extra_arguments": [ "-v" ] } ], "post-processors": [[ { "type": "docker-tag", "repository": "hypery2k/packer-openshift-demo", "tag": "latest" } ]] }
- Der Builder beschreibt das Basis-Image und grundlegende Anpassungen wie Volumes und Entrypoints
- Der Provisioner beschreibt die Ansible-Konfiguration
- Im Post-Processor wird der Docker-Tag für das Image festgelegt.
Die Hauptarbeit erledigt dabei aber das Ansible Playbook:
--- - name: Prepare | Setup Ansible runtime hosts: all gather_facts: no tasks: - name: Boostrap python raw: test -e /usr/bin/python || (apt-get -y update && apt-get install -y python-minimal) - name: Provision hosts: all tasks: - name: Install init system apt: name: dumb-init state: present - name: Put runtime programs copy: src: files/{{ item }} dest: /usr/local/bin/{{ item }} mode: 0755 owner: root group: root with_items: - docker-entrypoint.sh - name: CleanUp | Remove package artificats and Ansible hosts: all gather_facts: no tasks: - name: Remove python raw: apt-get purge -y python-minimal && apt-get autoremove -y - name: Remove apt lists raw: rm -rf /var/lib/apt/lists/*
Vor dem Ausführen von Ansible Tasks installieren wir Ansible innerhalb des Docker-Images und entfernen es anschließend wieder.
Im Beispielprojekt besteht die Konfiguration des Images daraus, Dumb-Init (ein minimales Init-System für Docker) hinzuzufügen und das Entrypoint-Shell-Skript zu kopieren.
Um das Docker Image zu erzeugen, wird lediglich ein packer build build.json benötigt:
==> docker: Creating a temporary directory for sharing data… ==> docker: Pulling Docker image: jenkinsci/ssh-slave:latest docker: latest: Pulling from jenkinsci/ssh-slave docker: Digest: sha256:bcade2596c695978bf62105a16c0cfe24ebbf72bed0c757d1aec691f005df176 docker: Status: Image is up to date for jenkinsci/ssh-slave:latest ==> docker: Starting docker container… docker: Run command: docker run -v /Users/mreinhardt/.packer.d/tmp/packer-docker670624880:/packer-files -d -i -t jenkinsci/ssh-slave:latest /bin/bash docker: Container ID: 99f3efab8a74493b7a3b74d2bab42b463556b36487364c6b057622a6cb749b25 ==> docker: Using docker communicator to connect: 172.17.0.2 ==> docker: Provisioning with Ansible… ==> docker: Executing Ansible: ansible-playbook - extra-vars docker: /var/folders/vp/2rx0yjfd4w12rx5b5y5bcbsc0000gn/T/packer-provisioner-ansible875281827 did not meet script requirements, check plugin documentation if this is unexpected docker: ________________________________________ docker: < PLAY [Prepare | Setup Ansible runtime] > docker: - - - - - - - - - - - - - - - - - - - - docker: ________________________ docker: < TASK [Boostrap python] > docker: - - - - - - - - - - - - docker: changed: [default] => {"changed": true, "rc": 0, "stderr": "Warning: Permanently added '[127.0.0.1]:63005' (RSA) to the list of known hosts.\r\nShared connection to 127.0.0.1 closed.\r\n", "stderr_lines": ["Warning: Permanently added '[127.0.0.1]:63005' (RSA) to the list of known hosts.", "Shared connection to 127.0.0.1 closed."], "stdout": "", "stdout_lines": []} docker: __________________ docker: < PLAY [Provision] > docker: - - - - - - - - - docker: docker: ________________________ docker: < TASK [Gathering Facts] > docker: - - - - - - - - - - - - docker: docker: ok: [default] docker: ____________________________ docker: < TASK [Install init system] > docker: - - - - - - - - - - - - - - docker: docker: changed: [default] => {"cache_update_time": 1552918991, "cache_updated": false, "changed": true, "stderr": "debconf: delaying package … docker: _____________________________ docker: < TASK [Put runtime programs] > docker: - - - - - - - - - - - - - - - docker: changed: [default] => (item=docker-entrypoint.sh) => {"changed": true, "checksum": "5d210e2bae060e6c5a5e6046f0ad7282c728b767", "dest": "/usr/local/bin/docker-entrypoint.sh", "gid": 0, "group": "root", "item": "docker-entrypoint.sh", "md5sum": "a622f4b8bc51e2e2cc90f5b800f767ad", "mode": "0755", "owner": "root", "size": 91, "src": "/tmp//ansible/ansible-tmp-1552918998.39–278279285350933/source", "state": "file", "uid": 0} docker: ________________________________________________________ docker: < PLAY [CleanUp | Remove package artificats and Ansible] > docker: - - - - - - - - - - - - - - - - - - - - - - - - - - - - docker: ______________________ docker: < TASK [Remove python] > … docker: - - - - - - - - - - - docker: < TASK [Remove apt lists] > docker: - - - - - - - - - - - - - docker: changed: [default] => {"changed": true, "rc": 0, "stderr": "Shared connection to 127.0.0.1 closed.\r\n", "stderr_lines": ["Shared connection to 127.0.0.1 closed."], "stdout": "", "stdout_lines": []} docker: ____________ docker: < PLAY RECAP > docker: - - - - - - docker: default : ok=6 changed=5 unreachable=0 failed=0 docker: ==> docker: Committing the container docker: Image ID: sha256:0a585c2d3f24283ab3620fa39aebad4d570f2e8748549559d78c4e006b9c3db2 ==> docker: Killing the container: 99f3efab8a74493b7a3b74d2bab42b463556b36487364c6b057622a6cb749b25 ==> docker: Running post-processor: docker-tag docker (docker-tag): Tagging image: sha256:0a585c2d3f24283ab3620fa39aebad4d570f2e8748549559d78c4e006b9c3db2 docker (docker-tag): Repository: holisticonag/packer-demo:latest Build 'docker' finished.
In diesem einfachen Beispiel ist der Packer ein totaler Overkill, dennoch zeigen sich die prinzipiellen Vorteile:
- Wir können bestehende Playbooks wiederverwenden oder ganz einfach eine virtuelle Maschine anstelle eines Docker-Images erstellen
- Die Wartung ist erleichtert, weil wir lesbare Install Tasks erstellen können, anstatt nur darauf zu achten, wenige Docker-Layer zu erstellen
- Durch die Verwendung von Ansible Playbook ist es erweiterbar, da wir jetzt viele bestehende Ansible Rollen verwenden können
Dennoch sollte man bedenken, dass ggf. zwei neue Werkzeuge erlernt werden müssen: Ansible und Packer.
Der vollständige Code ist auf GitHub verfügbar.
In einem weiteren Artikel werde ich mehr auf die Sicherheitsaspekte von Docker-Images eingehen.
weiterlesen