14 September 2017 ~ 0 Comments

Ein fauler Admin ist ein guter Admin

Auf einem Kundenevent zum Thema DevOps hatte ich bereits die Möglichkeit über den Nutzen von Automatisierung mit Hilfe von Ansible und Docker zu reden. An dieser Stelle möchte ich meinen Artikel mit einem Zitat von Bill Gates einleiten:

„Wenn ich die Wahl zwischen einem faulen Menschen und einem hoch motiviertem habe, würde ich immer den faulen nehmen.“1

Insbesondere für Administratoren ist dieses Zitat meiner Meinung nach mehr als nur zutreffend. Ein Beispiel für einen derartigen „faulen Mitarbeiter“ stellt der in folgendem Artikel beschriebene Entwickler, der seinen kompletten Arbeitsalltag gescriptet und automatisiert hat, dar: http://engineerpal.com/programmer-automates-everything/

Ebenso wie dieser Mitarbeiter alle seine Aufgaben mit Skripten automatisiert und reproduzierbar gemacht hat, muss auch ein fauler Admin seine Arbeit größtenteils in Skripten und Tools automatisieren. Um faule Admins sein zu können, haben wir unsere Arbeit mit Hilfe von Ansible2 verskripted und automatisiert sowie die meisten Anwendungen mit Docker reproduzierbar ausgerollt. Im Rahmen meines Beitrags beschreibe ich verschiedene Use-Cases in denen wir Ansible und Docker3 einsetzen und liefere verschiedene Beispiele sowie Skript-Auszüge.

Von guten und bösen DevOpslern

Mein erstes Beispiel stellt die interne Anwendungsenwicklung dar. Wir als IT-Abteilung betreiben nicht nur verschiedene Dienste für die eigene Firma und administrieren Dinge, wir entwickeln selbst auch Anwendungen. Diese Aufgabenvielfalt macht uns zu einem perfekten Beispiel für DevOpsler.

Eine typische Unterhaltung, wie man sie zwischen Entwicklern und Admins oft hört, endet erfahrungsgemäß mit dem Worten:
Ja aber bei mir läuft es doch!

8429-hd.jpg.jpg
Um eine häufige Ursache dieses Problems, nämlich unterschiede zwischen Entwicklungs-, Test- und Produktivumgebung, vermeiden zu können, haben wir uns entschieden unsere Anwendungen ausschließlich mittels Docker-Umgebungen zu entwickeln sowie auszurollen. So hat beispielsweise der PHP-Entwickler ein Setup aus ein bis zwei Docker-Containern. Im ersten Container laufen der Webserver und PHP-Interpreter, im zweiten – optionalen – Container die Datenbank. Der Code des Entwickler wird während der Entwicklung schlicht mittels Mount für den Docker-Container verfügbar gemacht.
Ist der erste Entwicklungsschritt abgeschlossen und die Anwendung soll getestet werden, wird nicht mehr mit lokalen Mounts gearbeitet. Ab diesem Zeitpunkt existiert ein dedizierter Docker-Container für die Anwendung. Dieser wird mit Hilfe eines Continous Integration Systems gebaut und publiziert.
Bei jedem Commit in unser Git (Bitbucket4) wird in unser CI-System (Bamboo5) ein neuer Build eines entsprechenden Docker-Containers ausgelöst der den Code in den entsprechenden Webserver-Container hineinkopiert. Währenddessen führt Bamboo auch die entsprechenden Unit-Tests (sofern verfügbar) aus. Ist der Anwendungscontainer fertig gebaut und mit einem Tag/Label versehen veröffentlicht, wird für den manuellen Integrationstest der Container gestartet und auf seine Funktionalität getestet.
Beim Integrationstest findet in der Regel lediglich ein Blackbox-Test statt, der die Änderungen betrifft, sowie stichprobenartig die Funktionalität an weiteren Stellen testet. (Natürlich kann man hier mit Hilfe von Oberflächentesttools weiter automatisieren). Ist der Test erfolgreich wird in Bamboo ein entsprechendes Release erstellt und der Container mit dem Tag „latest“ veröffentlicht, findet während dem Wartungsfester dann der Roll-Out des neuen Containers statt.

Start und Ausrollen einer Anwendung
ansible-playbook -i environments/production/inventory.yml -e "env=production state=started" www.it-economics.de.yml -s

Für den Roll-Out unserer Anwendungen nutzen wir Ansible um die Notwendigkeit manuellen Eingreifens auf unseren Servern zu minimieren. In dem entsprechenden Ansible-Playbook sind alle notwendigen Rollen, Konfigurationen und in verschlüsselter Form verwendete Zugangsdaten sowie Kinfigurationsdateien hinterlegt. In unseren Anwendungen gehen wir dabei davon aus, dass es eine strikte Trennung zwischen Daten und der Anwendung selbst gibt. Dies erlaubt es den Anwendungscontainer jederzeit wegzuwerfen und durch einen neueren ersetzen zu können.

Fragment der docker_app Rolle
---
- name: Pull updated containers
  command: docker pull {{ docker_repository }}/{{ app.image }}:{{ app.version }}
  when: pull == "always"
- set_fact:
    app_db:
      - "{{ app.name }}_db:database"
  when: app.db is defined and mysqld is defined
- set_fact:
    app_data:
      - "{{ app.name}}_data"
- name: Control App container
  docker:
    image: "{{ docker_repository }}/{{ app.image }}:{{ app.version }}"
    name: "{{ app.name }}"
    state: "{{ state }}"
    env: "{{ app.env|default({}) }}"
    expose: "{{ app.expose_ports|default([]) }}"
    ports: "{{ app.aux_ports|default([]) }}"
    volumes: "{{  app.volumes|default([]) }}"
    volumes_from: "{{ app_data }}"
    links: "{{ app_db|default([]) + app.links|default([])}}"
    restart_policy: "{{ docker_restart_policy }}"
    log_driver: "{{ log_driver }}"
    log_opt: "{%if log_driver == 'awslogs' %}{{ app_log_opt }}{% else %}{}{% endif %}"
    labels:
      traefik.backend: "{{ app.name }}"
      traefik.frontend.rule: "Host:{{ app.hostname|default(app.name) }}"
      traefik.port: "{{ app.port }}"

Um eine entsprechende Sauberkeit unserer Ansible-Repositories einhalten zu können, haben wir eine entsprechende Trennung der Environments/Umgebungen eingeführt. So nutzen wir intern zwei Stages – Testing und Production – sowie für jedes Kundensystem separat verschlüsselte eigene Umgebungen.

Applikationsupdates

Automation for the win – Wie eingehend beschrieben ist auch die Erstellung unserer Docker-Container voll automatisiert. Steht die Entscheidung, eine Anwendung in Docker umzusetzen, dann wird entweder dem Repository der Anwendung direkt ein Dockerfile hinzugefügt – oder aber einer der offiziellen Container eingesetzt. Der Bau unserer eingesetzten Dockercontainer erfolgt automatisiert wöchentlich sowie bei Änderungen des Dockerfiles oder der zu Grunde liegenden Basisimages. Hierdurch stellen wir sicher, dass die Images stets einen aktuellen Updatestand besitzen. Der Rollout der Anwendungen erfolgt durch Ansible und nur manuell, nachdem die aktualisierte Applikation entsprechend getestet wurde. Für den ausführlichen Test wird in der Regel ein Klon der Applikationsdaten erstellt und ein Test basierend auf den geklonten Daten durchgeführt.

Update und Neustart aller Anwendungen
ansible-playbook -i environments/production/inventory.yml -e "env=production state=restarted pull=allways" control_all_instances.yml -s

Muss es immer ein Microservice sein?

Eine grundlegende Empfehlung bei der Erstellung von Docker Images ist Dienste zu kapseln, also nicht Applikation und Datenbank im gleichen Container zu betreiben. Das ist aber an vielen Stellen nicht möglich. Daher zeigt sich wie bei Allem – man muss das richtige Maß finden – nur weil Microservices hip sind heißt es nicht, dass man sie immer einsetzen muss. Ein schönes schlechtes Beispiel für einen überladenen Container ist mein Admin-Arbeits-Image (mit und ohne GUI).

Dockerfile für den Admin-Workspace
FROM       felixkazuyade/baseimage
MAINTAINER Felix Kazyua
# Umgebungsvariablen
ENV ROOT_PASSWORD ChangeMeByEnv
ENV UBUNTU_PASSWORD ChangeMeByEnv
#Date of Build
RUN echo "Built at" $(date) > /etc/built_at
# Portfreigaben
EXPOSE 22
EXPOSE 80
EXPOSE 443
# Dateien reinkopieren
ADD entrypoint /entrypoint
#Konfiguration
RUN useradd -ms /bin/bash ubuntu
RUN adduser ubuntu sudo
# Anwendungen
RUN apt-get update
RUN apt-get update && apt-get install -y openssh-server git git-crypt ansible zsh tmux dialog apt-utils sudo
RUN mkdir /var/run/sshd
RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/'  /etc/ssh/sshd_config
RUN sed -i 's/PermitRootLogin PermitRootLogin prohibit-password/PermitRootLogin yes/'  /etc/ssh/sshd_config
# SSH login fix. Otherwise user is kicked off after login
RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
RUN chsh -s $(which zsh); sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"; exit 0;
#Specific User stuff
USER ubuntu
WORKDIR /home/ubuntu
RUN echo xfce4-session > /home/ubuntu/.xsession
RUN chsh -s $(which zsh); sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"; exit 0;
USER root
# Startbefehl
# Combining ENTRYPOINT and CMD allows you to specify the default executable for your image while also providing default arguments to that executable which may be overridden by the user.
# When both an ENTRYPOINT and CMD are specified, the CMD string(s) will be appended to the ENTRYPOINT in order to generate the container's command string. Remember that the CMD value can be easily overridden by supplying one or more arguments to `docker run` after the name of the image.
# Entrypoint explizit override needed
# Entrypoint needs new value for each argument ["/bin/ping","-c","4"]
# CMD Overwrite by arguments after run (same as Entrypoint ["localhost"])
RUN chmod +x /entrypoint
CMD ["/entrypoint"]

Entstanden ist das Image, da wir irgendwann genervt waren, dass jeder seinen eigenen Rechner in einem nicht reproduzierbaren Zustand hat. Ich nutze beispielsweise einen Mac, mein anderer Kollege hat Ubuntu 14.04 mit KDE und der nächste hat ein Ubuntu 16 mit einer anderen Oberfläche. Somit ist es ziemlich schwer einen funktionierenden und für alle halbwegs gleichen Softwarestand zu haben. Daher gibt es nun en Image mit RDP-Server, Git, Git-Crypt und ansible, so dass wir im Zweifel immer eine funktionierende Umgebung haben. Ein Beispiel für sinnvolle Microservices sind Datenbank- oder Webserver. Hier können wir die Dienste nach Maß kombinieren. In meinem Repository auf Dockerhub stelle ich einige Beispiele für Images sowie die entsprechenden Dockerfiles bereit:

Hier finden sich ebenfalls verschiedene Atlassian-Anwendungen.

Docker, SSL und Webdienste

Eine der größten Überlegenen die wir hatten war:
Wie können wir die Flexibilität und Dynamik von Docker mit Webservices kombinieren.
Natürlich nutzen wir für alle unserer Dienste verpflichtend SSL, also woher kommt das Zertifikat und wie erfährt der Webserver, dass er einen Reverseproxy für verschiedene Container machen muss? Hier haben wir uns auf eine Kombination aus Let‘s Encrypt und dem leichtgewichtigen Reverseproxy Træfik6 entschieden. Træfik benötigt hierfür Zugriff auf das Docker Socks-File. Durch hinzufügen der passenden Labels erfährt Traefik unter welcher Domain welcher Dienst erreichbar sein soll. Beim ersten Dienstaufruf wird bei Let‘s encrypt das SSL-Zertzifikat beantragt. An dieser Stelle sei angemerkt – ja, ein Wildcard-Zertifikat wäre einfacher und wir haben auch Wildcard Zertifikate, aber für jeden Dienst, insbesondere für Test-Umgebungen möchten wir nicht auf allen Servern das gleiche Zertifikat einsetzen.
An dieser Stelle muss jedoch die wesentliche Schwachstelle von Let’s Encrypt hervorgeben werden. Das Limit der Zertifikate pro Domain. Nutzt man wie in unserem Use Case eine Hauptdomain und unter dieser verschiedene Subdomains für alle Demos und Anwendungen, so läuft man sehr schnell in das Limit der 10 Zertifikate pro Woche und ist ab diesem Moment quasi Handlungsunfähig. Daher ist allen Anwendern an dieser Stelle dringend anzuraten bei Nutzung von Let’s Encrypt Produktiv- und Test-Domains zu Trennen.

Resümee

Ich hoffe ich konnte mit meinem Artikel den Nutzen von Automatisierung und die Sinnigkeit von Reproduzierbarkeit erfolgreich darstellen. Sofern Interesse an einer Weiterführung des Artikels besteht freue ich mich über Feedback.

1: https://www.goodreads.com/quotes/568877-i-choose-a-lazy-person-to-do-a-hard-job
2: https://www.ansible.com
3: https://www.docker.com
4: https://de.atlassian.com/software/bitbucket
5: https://de.atlassian.com/software/bamboo
6: https://traefik.io

Leave a Reply

%d Bloggern gefällt das: