Entwickeln von Containern - Best practice

Es soll ein Container mit einer eigenen Applikation erstellt werden. Diese Liste soll Optionen und Features aufzeigen, was mit und in Containern möglich ist.

Stichpunktsammlung

  1. Ausgangsbasis für den neuen Containers wählen
  2. Init-System wählen
  3. Updatefähig bleiben und /data/etc nutzen
  4. Rechte beschränken, eigene User anlegen
  5. Umgang mit Flash, Nutzen von TMPFS
  6. Version setzen
  7. Sichern der Entwicklungsstände
  8. RAM-Verbrauch kontrollieren
  9. Speicherplatz-Verbrauch kontrollieren

Hinweise zu den Stichpunkten

  1. Ausgangsbasis für den neuen Containers wählen
    Als Ausgangsbasis für neue Container kann dienen:
    • ein vom Router erstellter Container
    • ein auf dem PC erstellter Container
    • ein Basiscontainer, der von Dritten erstellt wurde (Debian, Python).
    Zum Ausprobieren und für schnelle Erfolge ist mit Sicherheit ein vom Router erstellter Container besser geeignet. Es ist jedoch ein Nachteil, dass der Inhalt des Containers aus Teilen der Router-Firmware zusammengesetzt ist. Der Inhalt ist also abhängig davon, mit welcher Firmwareversion der Container erstellt wurde. Die Dateien im Container sind zum Großteil Binärdateien, die Sourcen liegen nicht direkt vor.

    Ein auf dem PC erstellter Container bedarf erst etwas mehr an Aufwand. Hilfreich sind ist das SDK und Repository mit Build-Skripten. Die Build-Skripte und Listen, die die Zusammensetzung von Containern definieren, können leicht abgeändert werden. Der große Vorteil ist, dass ein Container unabhängig von der Version der Routerfirmware jederzeit nachvollziehbar und wiederholbar bleibt. Spätestens dann, wenn eigene Programme cross compiliert werden müssen, bedarf es der Installationen eines SDKs.

    Wenn die eigene Applikation mit einer Skript-Sparche (z.B. Python, Bash) erstellt werden soll, kann ein externer Basiscontainer als Ausgangsbasis nützlich sein. Die Entwicklung der Applikation kann direkt im Container erfolgen und sofort getestet werden. Nachteilig ist, dass die Zusammensetzung der Container nicht nachvollzogen werden kann. Pakete im Container können nicht so leicht aktualisiert werden. Außerdem enthalten solche Container meist viel Inhalt, der gar nicht benötigt wird. Diese Container sind meist sehr groß und verbrauchen relativ viel Resourcen (RAM, Flash, CPU-Zeit).

  2. Init-System wählen
    Im Container kann nahezu jedes beliebige Init-System eingesetzt werden. In vom Router erstellten Containern wird "finit" eingesetzt. Ein einfaches init bringt auch "busybox" mit.

    Bei finit besteht die ganze Konfiguration aus der Datei "/etc/finit.conf". Zeilen, die mit "#" beginnen, sind Kommentare und werden ignoriert. Jede Zeile beginnt mit einem Schlüsselwort "run" oder "service". Zeilen mit "run" werden einmalig beim Containerstart ausgeführt. Erst wenn der Befehl ausgeführt wurde, wird mit der nächsten Zeile von finit.conf fortgefahren. Einträge mit "service" starten Applikationen, die überwacht werden. Beendet sich eine solche Applikation, wird sie sofort wieder gestartet. Die Applikation darf sich nicht in den Hintergrund schieben (daemonize), sondern muss im Vordergrund laufen. Ansonsten nimmt finit an, dass sich die Applikation beendet hat und startet sie neu.

    Bei init aus der busybox steht die Datei "/etc/inittab" zur Verfügung. Hier wird meist mit Hilfe von ash-Skripten alles gestartet und gestoppt.

    Ein Container kann auch aus nur einer einzigen statisch gelinkten Datei bestehen. In dem Fall ist überhaupt kein init-System notwendig. Mit der Sprache "go" lassen sich solche Applikationen sehr schnell und einfach erstellen. Die Applikation muss im Container den Dateinamen "/sbin/init" erhalten. Beendet sich die Applikation oder stürzt sie ab, wird das vom Router erkannt. Sie Router startet den Container dann sofort erneut. So können sehr kleine Container entstehen, die sehr wenig Angriffspläche für Hacker-Attacken bieten.

  3. Updatefähig bleiben und /data/etc nutzen
    Wenn ein Container mit dem gleichen Bezeichner erneut in den Router geladen wird, überschreibt dieser den Inhalt des alten Containers. Die Konfiguration des Containers im Profil des Rotuers wird nicht verändert. Dadurch ist es möglich, den Inhalt eines Container zu aktualisieren um neue Features anzubieten, Fehler zu beheben oder Sicherheitslücken zu stopfen.

    Wenn ein Container überschrieben wird, werden auch die darin angesammelten Daten überschrieben wie z.B. Log-Dateien oder Konifgurationsdateien. Damit solche Dateien einen Containerupdate überstehen, erhält jeder Container ein Verzeichnis "/data". Der Inhalt dieses Verzeichnisses wird nicht gelöscht, wenn ein Container überschrieben wird. Es wird nur dann gelöscht, wenn der komplette Container gelöscht wird.

    In vom Router erstellten Containern befinden sich in "/data" bereits die Unterverzeichnisse "/data/etc" und "/data/log". "/data/log" bietet sich für Log-Dateien an, die ein Containerupdate nicht entfernen soll, sofern das gewünscht ist. "/data/etc" kann für Konfigurationsdateien benutzt werden. Wenn bei der Containerkonfiguration die Checkbox "Inhalt von /data/etc als Archiv dem Profil anfügen" aktiviert wurde, wird der Inhalt von dem Verzeichnis gemeinsam mit dem Profil in einem Update-Paket gespeichert, das der Benutzer vom Router herunterladen kann. Das hat den Zweck, dass mit nur einem Download eine Router-Konfiguration zusammen mit der Container-Konfiguration gesichert werden kann. Beim Upload eines solchen Update-Paketes auf den Router wird der Inhalt der Containerkonfiguration wieder nach "/data/etc" kopiert. Voraussetzung dafür ist, dass der Container immer noch unter dem gleichen Bezeichner auf dem Router vorhanden ist.

    Wichtig! Wird dieser Mechanismus genutzt, muss der Entwickler des Containers dafür sorgen, dass neuere Versionen des Container auch mit eventuell veralteten Konfigurationsdateien umgehen kann.

    Für alle Dateien in "/data" gilt das gleiche wie für die Dateien im eigentlichen Container: Die Anzahl und die Größe der Dateien ist nicht künstlich limitiert. Der Nutzer im Container muss sicherstellen, dass z.B. die Log-Dateien darin rotiert und alte Dateien gelöscht werden.

  4. Rechte beschränken, eigene User anlegen
    In Containern können eigene Benutzer (adduser) und Benutzergruppen (addgroup) angelegt werden. Das ist für alle Container empfehlenswert. Das gilt erst recht für Applikationen, die Dienste im WAN anbieten. Ein Angreifer kann eine eventuell vorhandene Schwachstelle in der Applikation ausnutzen. Läuft diese mit root-Rechten, erhält der Angreifer vielleicht auch root-Rechte und kann die Kontrolle über den kompletten Containerinhalt erlangen. Erhält er nur Rechte eines nicht privilegierten users, ist der Angriff etwas weniger fatal.

  5. Umgang mit Flash, nutzen von TMPFS
    Oft sind temporäte Dateien das Mittel der Wahl, um Daten kurzfristig abzuspeichern. Das Speichern von Dateien auf dem Flash-Baustein verbraucht relativ viel Rechenleistung und ist im Vergleich zum Speichern im RAM sehr langsam. Der Grund dafür ist, dass bei Flash-Speichern immer "wear leveling" betrieben werden muss, weil die Anzahl der maximalen Speichervorgänge endlich ist. Eine Lösung ist, temporäre Dateien in einem TMPFS (Temporary File System) wie "/tmp" abzulegen. Vom Router erstellte Container legen das Verzeichnis an und mounten ein 20 MByte großes TMPFS (konfigurierbar in "/etc/finit.conf"). Das Speichern von Dateien in so einem TMPFs ist ähnlich schnell wie das Speichern im RAM. Die Dateien darin sind aber nicht dauerhaft gespeichert, spätestens nach einem Containerneustart sind sie weg.

  6. Version setzen
    Die Firmware des Routers kümmert sich nicht um eine Versionierung der Container. Lediglich der Bezeichner und der Containername sind wichtig. Für eine mögliche Versionierung ist der Ersteller des Containers selber verantwortlich. Bei vom Router erstellten Containern wird die Datei "/usr/share/version" angelegt. Die Datei "/usr/share/image_name" ist für eine Versionierung ungeeignet! In dieser Datei ist der Bezeichner des Containers gespeichert. Der Bezeichner verknü,pft den Containerinhalt mit den Einstellungen im Profil. Wird diese Datei im Container verändert, wird der Container sehr wahrscheinlich nicht mehr gestartet.

  7. Sichern der Entwicklungsstände
    Wenn die Entwicklung der Applikation im Container selber erfolgt, ist eine regelmäßige Sicherung des Containerinhalts unverzichtbar (siehe Ausgangsbasis). Geeignete Werkzeuge hierfür sind "scp" oder "rsync". Am Sichersten ist es, nicht im Container selbst Dateien zu erstellen oder zu modifizieren, sondern das auf dem PC zu machen. Die Dateien könenn zum Testen leicht von einem lokalen HTTP-Server und einem im Container aufgerufenen "wget" in den Container geholt werden. Lokal können die Sourcen dann in einem git-Repository verwaltet werden.

  8. RAM-Verbrauch kontrollieren
    Container müssen in der Regel für lange Zeit zuverlässig und autonom ihren Dienst verrichten. Deshalb ist es sehr wichtig, mögliche Speicherlecks zu vermeiden und die fertige Applikation auf Speicherlecks zu testen. Mit "top" kann die Prozessliste aufgerufen werden und der RAM-Verbrauch der einzelnen Applikationen beobachtet werden. Applikationen sollten in Dauertests laufen, ihr RAM-Verbrauch sollte dabei nicht unendlich steigen.

  9. Speicherplatz-Verbrauch kontrollieren
    In den Containern gibt es keine künstliche Limitierung (Qutoa) des zur Verfügung stehenden Flash-Speichers. Die Betreiber der Container sind verantwortlich dafür, dass z.B. Log-Dateien regelmäßig gekürzt oder gelöscht werden.