Containers

Du kannst bei uns auch Container hosten lassen um dein Projekt laufen zu lassen. Grundsätzlich mappen wir ein Webhosting (d.h. ein Zugang mit Domain) auf einen Container resp. auf ein Set von Containers, die als Pod betrieben werden. Im folgenden sprechen wir grundsätzlich von einem Pod, wobei hierbei ein oder mehrere Container des gleichen Webhostings gemeint sind.

Die Integration in unser Webhosting nimmt dir dabei einiges ab, stellt jedoch auch ein paar Anforderungen an deine Container. Wenn deine Applikation im Container mit diesen Anforderungen und Einschränkungen umgehen kann, wird ein Hosten deiner Container ohne weiteres möglich sein. Weitergehende Anforderungen müssen individuell angesehen werden.

Dein Container Hosting wird nach wie vor durch unseren Webserver ausgeliefert, es ist deshalb nicht zwingend notwendig in deinem Pod einen Webserver zu betreiben.

Übersicht

Damit die Inhalte deines Containers abrufbar werden, routen wir die Domains zu deinem Hosting auf einen Port deines Pods. Wir machen weiterhin die Organisation der Zertifikate (wie auch deren Rotation), terminieren HTTPS vor deinem Container und reichen nur HTTP Traffic in deinen Container.

Als Definition wie dein Pod aussehen und damit soll wird eine Spezifikation deines Pods im Pod-Yaml Format von Kubernetes verwendet, wobei nur ein Subset an Optionen beachtet werden und einige Einstellungen durch uns forciert werden. Das praktischste ist es ein solches Pod-YAML lokal bei dir mit Hilfe von podman zu erstellen und dann mit uns zur initialen Einrichtung zu teilen. Solltest du nur einen simplen einfachen Container haben, dann reicht es auch, einfach zu wissen auf welchem Port dieser Pod hört.

Anforderungen

Neben dem Pod-YAML oder der Container Informationen gibt es folgende Anforderungen:

  • Images sind in einer Registry verfügbar. Sind die Images nur mit Hilfe von Authentifierung zugänglich, benötigen wir Zugangsinformationen mit Pullrechten. Siehe dazu Registry Authentifizierung.
  • Keine Prozesse unter root. Die ausgeführten Prozesse dürfen nicht als root laufen. Es gibt kaum Gründe, weshalb ein Prozess in einem Container als root laufen soll.
  • Ingress HTTP Traffic. Wir forwarden den Traffic auf euer Webhosting auf einen vordefinierten Port (Bevorzugt 8080). Wir terminieren HTTPS auf dem Host selbst und in den Pod wird nur HTTP weitergeleitet.
  • Storage ist relativ zu eurem Webhosting.
  • Sämtliche Volumes eines Container Image müssen abgebildet oder explizit ignoriert werden.
  • Container laufen grundsätzlich read-only, dies bedeutet sämtliche Ordnerstrukturen, welche veränderbar sein müssen, müssen über ein Volume eingebunden werden.

Webhosting Struktur

www/ # Public Webhosting Verzeichnis -> Siehe Ingress HTTP Traffic
logs/ # Sämtliche Logs zu eurem Hosting > Siehe Logs
scripts/ # User Skripte, die euch bei der Verwaltung helfen
data/ # Vom Webhost zugreifbare Struktur, sieh Storage
private/ # Privater Space des Webhostings, siehe Konfiguration
tmp/ # Temporäre Dateien, die während der Ausführung verwendet werden

Konfiguration

Dein Container Webhosting besteht üblichersweise aus einem Pod, dieser Pod hat den Namen deines Webhostings.

Relevant hierfür sind zwei Dateien:

private/container-config/pod-<POD-NAME>.yaml
private/container-config/system-<POD-NAME>.yaml

Die Datei system-<POD-NAME>.yaml ist durch uns vorgeben und nicht editierbar. Sie dokumentiert gewisse Einstellungen, welche wir beim starten deines Containers enforcen.

Pod YAML

Die Datei private/container-config/pod-<POD-NAME>.yaml ist eine durch dich editierbare Definition deines Pods. Die Spezifikation folgt dem Pod-Yaml Format von Kubernetes. Wobei jedoch nur ein Subset an Controls erlaubt sind und ansonsten durch uns forciert werden.

Dies sind:

  • spec.volumes - Wobei nur Directory, File und emptyDir mit medium tmpfs unterstützt werden. Siehe Storage für Details.
  • spec.hostname - Wird per default auf deinen Hostingnamen gesetzt
  • spec.securityContext.runAsUser - Wobei dies nicht root sein darf
  • spec.securityContext.runAsGroup - Wobei diese ggf. überschrieben wird. Siehe Storage
  • spec.containers - Wobei von der ContainerSpec nur folgende Attribute unterstützt werden:
    • name: notwendig!
    • image: notwendig!
    • volumeMounts: Referenziert auf spec.volumes - Siehe Einschränkungen Storage
    • env: Mehr dazu siehe Abschnitt Env
    • ports: Wobei hier nur Ports erlaubt sind, welche in der Datei system-<POD-NAME>.yaml freigeschalten sind. Siehe Ingress HTTP Traffic für mehr.
    • securityContext.runAsUser - Wobei dies nicht root sein darf
    • securityContext.runAsGroup - Wobei diese ggf. überschrieben wird. Siehe Storage

Alle anderen Attribute eines Pod Yaml Spec werden ignoriert.

Applikationscode

Hast du Applikationscode, welcher nicht Teil des Images ist, dann kannst du diesen auch in private/app hinterlegen. Dieser Ordner kann dann unter /app in deinen Container gemapped werden. Dies ermöglicht es bspw. im Container selbst nur die Runtime zu haben und die App (bspw. deinen PHP/Python/Ruby/…) code separat zu verwalten.

Ingress HTTP Traffic

Wie bereits erwähnt regeln wir die SSL Zertifikate und terminieren HTTPS auf dem Host, wo der Pod läuft. Wir routen den Traffic für dein Hosting auf den vordefinierten Port in deinen Pod. Dieser Port muss durch einen Container in deinem Pod definiert worden sein. Alle anderen Ports werden ignoriert.

Generiert deine Applikation im Container Daten, welche direkt durch unseren Webserver (bspw. Bilder) ausgeliefert werden können, so können wir bspw. den Pfad /images vom Routing auf deinen Container aussnehmen und diese Dateien direkt ausliefern. Dies spart resourcen und ist sicher schneller als den Umweg durch deinen Container Prozess zu nehmen.

Storage

Über spec.volumes kannst du Pfade anziehen, welche in die Containers deines Pods gemounted werden. Wichtig hierbei ist, dass die Pfade immer relativ zum Pfad deines Webhostings sein müssen. Wir empfehlen grundsätzlich alle Pfade unter /data/private/data anzulegen. Bspw. so:

  volumes:
    - name: db
      hostPath:
        path: /data/private/data/db
        type: Directory

Der Ordner muss existieren und darf kein Symlink sein. Die direkten Ordner der Webhosting Struktur (wie bspw. /www) einzubinden ist nicht unterstützt.

Benötigen Container in einem Pod Storage, so wird der Prozess mit einer von uns vorgegeben Gruppe ausgeführt. Der User wird jedoch beibehalten. Dies hat den Effekt, dass Dateien die durch die Prozesse in Containern geschrieben werden durch euren SFTP User les- oder gar editierbar sind, wenn die entsprechenden Gruppenberechtigungen vergeben sind. Soll ein Volume schreibbar sein, sollte dies also per SFTP chmod g+w erstellt werden.

Du kannst auch Dateien in deinen Container einbinden. Beispielsweise so:

  volumes:
    - name: db-dump
      hostPath:
        path: /data/private/data/db-dump.sh
        type: File

Die Datei muss existieren und ein richtige Datei (bspw. kein Symlink) sein.

Benötigst du nur flüchtigen Speicher, so kannst du dies folgendermassen spezifizieren:

  volumes:
    - name: db
      emptyDir:
        medium: Memory

Image Volumes

Container Images können selbst definieren, welche Pfade in ihrem Image als Volume verwendet werden soll. Das Standardverhalten von Container Runtimes (podman, docker, …) ist, dass diese als anonyme Volumes gemounted werden und quasi semi-persistent sind.

Bei uns werden diese Volume definition explizit ignoriert.

Wird versucht ein Image zu starten, dass Volumes definiert hat, die aber nicht in den Volumes auf lokalen Storage gemapped wird, so wird die Ausführung des Pods abgebrochen. Dies damit auch wirklich alle persistent Daten in einem Image an einem von euch definierten Ort sind und ihr alle anderen Volumes zum ignorieren freigegeben habt.

Ihr könnt diese Pfade entweder als flüchtigen Speicher definieren oder explizit ignorieren.

Letzeres kann folgendermassen passieren:

metadata:
  [...]
  volumes_to_ignore:
    '<container_name>':
      - /path/of/a/volume
      - /another/volume/path

Env

In der Pod Spezifikation kann du Environment Variablen für deinen Container festlegen. Es empfiehlt sich kleinere Konfigurationen wie bspw. Username und Passwort einer Datenbank darüber festzulegen.

Zusätzlich kannst du eine typsiche Env-Datei unter private/container-config/<POD-NAME>-<CONTAINER_NAME>.env hinterlegen, welche beim starten auch angezogen wird.

Registry

Sollten die Images der Container nicht anonym bezogen werden können, so wird ein Account mit Pull-Berechtigung auf der entsprechenden Registry benötigt (unter Gitlab ist das z.B. ein Project Access Token mit der Rolle Reporter und der Berechtigung read_registry) Die Authentifizierungsdaten werden aus private/container-config/auth-<POD_NAME>-registry.yaml gelesen und du kannst in dieser Datei Username und Passwort für mehrere Registries hinzufügen. Das Format der Datei ist folgendes:

registry.example.com:
  user: myreaduser
  password: some_password

Solltest du die initialen Authentifizierungsdaten nicht mehr benötigen, so teile uns dies mit. Diese müssen zusätzlich von uns von Hand entfernt werden.

Updates

Wir überprüfen die Images des Pods täglich auf Basis der hinterlegten Image Spezifikationen auf Updates. Ist für ein Image ein Update verfügbar, wird der Pod anschliessend neugestartet.

Grundsätzlich empfehlen wir die Images mit einem Tag zu referenzieren, welches Updates bekommt, jedoch eine stabile API bereitstellt. So dass die automatisierten Updates immer funktionieren.

Neustarten

Du kannst deinen Pod auch selbst neustarten. Hierfür kannst du das user script pod_restart anstossen. Dieses stoppt den aktuell laufenden Pod, worauf dieser automatisch wieder gestartet wird.

Das Skript wird durch anlegen der leeren Datei: scripts/pod_restart/pod_restart.run angestossen.

Beim Starten werden auch die Images neu heruntergeladen. Solltest du also dein Image schneller als das tägliche Update erneuern möchten, dann kannst du dies auch durch einen solchen Neustart erzwingen.

Logs

Es ist empfohlen, dass die Applikationen in Containern ihre Logs auf stdout/stderr schreiben. Dies hat den Vorteil, dass wir die Logs dann in deinem Webhosting unter logs/<hostingname>-<podname>.log hinterlegen können. Wir rotieren diese auch täglich. Diese Logs solltest du aber nie löschen, ansonsten musst du einen Tag warten, bis wieder Logs da rein geschrieben werden.

Wiederkehrende Jobs aka. CronJobs

Benötigst du wiederkehrende Jobs, welche in deinem Pod angestossen werden sollen, so können wir Cron Jobs einrichten, die regelmässig angestossen werden. Dafür müssen wir wissen, in welchem Container, welcher Pfad angestossen werden muss.

Beachte

Ein paar Punke, die es bei unserem Container Hosting zu beachten gibt:

E-Mail Versand

Musst du E-Mails aus deinen Containers verschicken, so musst dies über unsere Mailserver direkt machen. Die Auslieferung muss dabei über einen authentifizierten Account gehen. Wir empfehlen für dein Hosting einen dedizierten Account unter deiner Domain zuerstellen und dann für diesen ein Applikationspasswort zu erstellen. Mit diesem Account können E-Mails dann über smtp.immerda.ch:587 versendet werden.

Logische Backups

Grundsätzlich backupen wir wie bei allen anderen Hostings auch, alle Dateien unter deinem Hosting. Wenn du in deinem Pod bspw. jedoch auch eine Datenbank laufen lässt, dann wird ein reines Backup der Dateien nicht ausreichen. Du musst dafür ein Skript in einem deiner Container einrichten, welches das Backup der Datenbank in ein Verzeichnis deines Webhostings erstellt. Dieses Skript kann dann durch einen CronJob angestossen werden.

Beispiel

Hast du Applikationen als Container gebaut, die du bei uns hosten möchtest. Dann helfen dir evtl. folgende Schritte das ganze einrichten etwas zu beschleunigen und einige Probleme bereits im vorraus auszumerzen.

Du benötigst dafür:

Mit folgenden Schritte führen wir dies mit einem Simplen WebContainer aus, der illustrativ für ein simples Hosting herhalten soll:

$ cd /tmp && mkdir mycontainertest
$ curl 'https://code.immerda.ch/immerda/puppet-modules/podman/-/raw/master/files/manage-user-pod.rb?inline=false' -o manage-user-pod.rb
$ chmod +x manage-user-pod.rb
$ mkdir data data/app tmp tmp/run
$ chmod a+w tmp/run
$ cat >data/app/index.html<<EOF
Hello World
EOF
$ cat >pod.yaml<<EOF
apiVersion: v1
kind: Pod
metadata:
  name: mycontainertest
spec:
  containers:
  - env:
    - name: ADDR
      value: 127.0.0.1:8080
    image: registry.git.autistici.org/ai3/docker/static-content:latest
    name: mywebserver
    volumeMounts:
    - mountPath: /var/www
      name: data-app
    securityContext:
      runAsUser: 1000
      runAsGroup: 1000
    ports:
    - containerPort: 8080
      hostPort: 8080
      protocol: TCP
  volumes:
  - hostPath:
      path: /data/app
      type: Directory
    name: data-app
EOF

$ cat >system.yaml<<EOF
name: 'mycontainertest'
volumes_base_dir: '$(pwd)'
volumes_containers_gid_share: true
container_env_dir: '$(pwd)'
tmp_dir: '$(pwd)/tmp'
socket_ports:
  8080:
    dir: $(pwd)/tmp/run
exposed_ports: []
pidfile: '$(pwd)/pod.pid'
EOF

$ ./manage-user-pod.rb parse pod.yaml system.yaml
# alles ok?

$ ./manage-user-pod.rb start pod.yaml system.yaml
# pod wird gestartet, rufe nun den inhalt ab

$ curl --unix-socket tmp/run/8080 http://localhost
Hello World

$  ./manage-user-pod.rb stop pod.yaml system.yaml
# pod wird gestoppt