Cronjobs in non-root Docker containern

Ich habe mich gerade ungefähr 3 Stunden mit folgendem Problem befasst:

Ich wollte einen cronjob in meinem Solr-Index-Container einrichten.

Nun ist es so, dass ich prinzipiell durchaus weiß wie man cronjobs normalerweise einrichtet und an sich wäre das eine Sache von wenigen Minuten. Im Internet findet man etliche Beschreibungen, wie man cronjobs im Container einrichtet. Problem: Alle Tutorials, die ich gefunden habe, haben sich ausschließlich auf Container bezogen, welche als root ausgeführt werden.

Mein Problem: Mein Cronjob musste in einem Container eingerichtet werden (an sich nicht weiter schlimm) UND dieser Container sollte nicht als root user laufen (!).

Letzteres stellt die Schwierigkeit dar. Doch der Reihe nach:

Wie legt man normalerweise einen cronjob an?

  1. cron muss laufen (als Prozess). Das ist bei vielen Linux-Distributionen standardmäßig der Fall. Ansonsten muss man cron installieren und per cron starten.
  2. man legt den cronjob an. Hierzu gibt man cronjob -e ein und kann dann die gewünschten cronjobs anlegen. Diese werden dann in der entsprechenden crontab des aufrufenden users gespeichert und durch cron ausgeführt.

Wie man sieht, sind beide Punkte im Docker-Kosmos nicht trivial.

Probleme in Bezug auf Punkt 1:

  • Docker-Container sollen normalerweise nur einen Prozess ausführen. Der Index-Container soll den Index-Prozess (also solr) ausführen. Der cron-Prozess ist aber ein zweiter Prozess der parallel im Container laufen soll.
  • Docker-Container sollten aus Sicherheitsgründen nicht als root laufen, sondern als unprivilegierter Benutzer.

Aus einem praktischen Blickwinkel ergeben diese beiden Punkte ein wahres Dilemma: Der Prozess, den der Container ausführen soll wird per ENTRYPOINT oder CMD-Direktive im Dockerfile angegeben. Mein Dockerfile ohne den cronjob sehe beispielsweise so aus:

FROM solr:6.6
COPY wichtigeDatei /opt/solr/wichtigeDatei
... (weitere wichtige Dinge)
CMD solr-start && tail -f /dev/null

Gut soweit. Das funktioniert einwandfrei. Dann fügen wir mal den cronjob ein.

FROM solr:6.6
COPY wichtigeDatei /opt/solr/wichtigeDatei
COPY crontab /opt/solr/crontab
... (weitere wichtige Dinge)
RUN apt-get update \
&& apt-get -y install cron \
&& crontab /opt/solr/server/backup-cron \
&& rm /opt/solr/server/backup-cron \
&& cron
CMD solr-start && tail -f /dev/null

Was ist hier geschehen? In Zeile 3 kopiere ich eine Datei in welcher mein cronjob steht in das Container-Image. Vor der CMD-Direktive installiere ich cron, schreibe per crontab mein cronjob weg (im Gegensatz zu crontab -e welches einen interaktiven Editor startet – was ja im Dockerfile-Kontext schlecht ist – kann man mit crontab <Datei> auch eine Datei angeben, welche den cronjob beinhaltet, sodass man ihn nicht manuell eintippen muss) und führe dann cron aus.

Probleme:

  1. cron wird als solr ausgeführt. Das funktioniert so nicht. cron muss als root ausgeführt werden.
  2. cron wird VOR der CMD-Direktive aufgerufen. Das heißt (angenommen man könnte cron theoretisch per solr user starten): In dem temporären Container, der für die RUN-Zeile im Dockerfile gebaut wird, läuft cron. Aber sobald die Zeile des Dockerfiles abgearbeitet ist, läuft cron nicht mehr! Sobald die CMD-Direktive ausgeführt und der Container gestartet (und durch das tail am laufen gehalten) haben wir also kein laufendes cron mehr.

Was kann man da tun? Cron MUSS im CMD ausgeführt werden, und es MUSS als root ausgeführt werden. Das ergibt einen Widerspruch. Container sollte NIE als root laufen (und beispielsweise der Solr-Container stoppt aus Sicherheitsgründen sogar automatisch, wenn er als root-Benutzer gestartet wird). Wenn ich aber cron in den CMD-Befehl mit einbauen muss, habe ich keine Möglichkeit mehr, anschließend den Container als NICHT-root zu starten. Nach dem CMD kommt ja im Dockerfile nichts mehr.

Lösung:

Nach viel hin und her, bin ich zu folgender Lösung gekommen:

FROM solr:6.6
COPY wichtigeDatei /opt/solr/wichtigeDatei
COPY crontab /opt/solr/crontab
COPY --chown=solr indexStarter.sh /opt/solr/indexStarter.sh
... (weitere wichtige Dinge)
USER root
RUN apt-get update \
&& apt-get -y install cron \
&& crontab /opt/solr/server/backup-cron \
&& rm /opt/solr/server/backup-cron \
&& apt-get -y install sudo \
&& gpasswd -a solr sudo && \
echo "solr\tALL=(ALL:ALL) NOPASSWD: /usr/sbin/cron" >> /etc/sudoers
USER solr
CMD /bin/bash /opt/solr/indexStarter.sh

Vor der Erklärung nochmal kurz eine Rekapitulation des Problems: cron muss als Teil der CMD-Direktive ausgeführt werden. cron muss als root ausgeführt werden. Der Container darf aber nicht als root starten.

Jetzt zur Erklärung des finalen Dockerfiles:

Bevor ich cron installiere und den crontab-Befehl ausführe, wechsle ich zum root Benutzer. Das ist im Rahmen der Abarbeitung des Dockerfiles gar kein Problem – solange vor der CMD/Entrypoint-Direktive der Benutzer wieder gewechselt wird.

sudo to the rescue

Nachdem cron installiert ist, installiere ich auch noch sudo. Anschließend füge ich den solr-Benutzer der sudo-Gruppe hinzu. Das bewirkt, dass ich mit dem Benutzer solr Befehle ausführen kann, als ob dieser der Benutzer root wäre. Zuletzt füge ich eine Zeile ans Ende der Datei /etc/sudoers ein. Ohne diese Zeile wäre es zwar möglich, den cron-Befehl auszuführen. Dann würde aber nach Eingabe des Befehls (sudo cron) eine Passwortabfrage aufpoppen. Da ich diese im Rahmen des Dockerfiles nicht bearbeiten kann, sagt die obige Zeile, dass bei Ausführung des Befehls sudo cron KEINE Passwortabfrage auftaucht und der Befehl stattdessen einfach ausgeführt wird.

Anschließend wechsle ich im Dockerfile zum Benutzer solr zurück bevor die CMD-Direktive ausgeführt wird.

Die CMD-Direktive tut nichts anderes, als ein Script auszuführen. Das mache ich bei meinen Images derzeit ganz gerne. Das Startscript kann dann noch zusätzliche Konfigurationen ausführen (wie beispielsweise Zugangsdaten aus Docker-Secrets auslesen, Index-User konfigurieren, etc…). Das Script sieht dann letztlich wiefolgt aus (die für diesen Artikel irrelevanten Aufgaben innerhalb des Scripts habe ich rausgekürzt):

#!/bin/bash
sudo cron
/opt/solr/bin/solr start
tail -f /dev/null

Wie man sieht: Hier wird nun sudo cron ausgeführt. Anschließend wird solr gestartet und danach gibt es den obligatorischen tail auf /dev/null um den Container am Laufen zu halten.

Kurzzusammenfassung der Lösung (alle Schritte im Dockerfile zu erledigen):

  1. auf Benutzer root wechseln
  2. cron und sudo installieren
  3. gewünschten Benutzer der sudo-Gruppe hinzufügen
  4. cron in /etc/suoders als NOPASSWD deklarieren
  5. zurück auf gewünschten nicht-root-Benutzer wechseln
  6. In der CMD/Entrypoint-Direktive sudo cron ausführen (neben den weiteren benötigten Kommandos) – beispielsweise innerhalb eines Scripts oder auch als Befehlskette innerhalb von CMD.

2 Gedanken zu “Cronjobs in non-root Docker containern

    • Der cronjob ist für regelmäßige Backups meines Solr-Services zuständig. Prinzipiell wäre es sicher möglich den cronjob in einen anderen Container auszulagern und das Backup von dort zu starten (das Backup läuft über das triggern eines API-Endpunktes von Solr). Ich mache mir dazu nochmal Gedanken..auf der einen Seite entspricht das dem Dogma „Ein Container – eine Aufgabe / ein Prozess“.. andererseits hat der cronjob-Container dann ohne den Index-Container keine Existenzberechtigung mehr.. Ich weiß nicht was hier das kleinere Übel ist.. Aber vielen Dank für den Hinweis auf pid! Die pid-Option war mir bisher gar nicht geläufig. Damit werde ich mich definitiv mal näher befassen.

      Gefällt mir

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden /  Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden /  Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s