Kubernetes einfach mit rancher : Ghost CMS (Part 6)

In Kubernetes einfach mit rancher : Ghost CMS werden wir Ghost bereitstellen. Einmal als Helm Chart und einmal als Deployment.

Kubernetes einfach mit rancher : Ghost CMS ist der sechste und somit der letzte Part der Kubernetes einfach mit Rancher Serie. In diesem Beitrag, beschreibe ich die Installation und die Inbetriebnahme von Ghost.

In Kubernetes einfach mit rancher : Ghost CMS lernen wir nicht nur die Inbetriebnahme per Helm. Ich demonstriere in diesem Artikel ein einfaches deployment per yaml.

Was ist Ghost

Ghost ist eine moderne Publikationsplattform. Den Anfang machte Ghost mit einer Kickstarter Kampagne, und bewirbte sich als einfache Open-Source Blogging-Plattform Alternative. Mittlerweile ist man zwar immer noch weit von der Komplexität von WordPress entfernt, aber man ist auch nicht mehr die reine Blogging-Plattform.

Ghost hat sich zu einer interessanten Alternative für verschiedene Zwecke entwickelt. Selbst schmückt man sich mit der Aussage, dass man die Nummer 1 im headles nodeJS CMS Markt ist wozu ich überhaupt nichts sagen kann.

Ich verwende Ghost als reine Blogging Plattform, und das nicht einmal im Team. Daher kann ich zu allem was Ghost darüber hinaus verspricht kein bisschen meinen Senf abgeben 🙂 . Als Blogging-Plattform bin ich mit Ghost sehr zufrieden und kann es ohne Bedenken weiter empfehlen.

So, dieser Artikel soll nur am Rande Ghost behandeln. Das Thema ist die Inbetriebnahme innerhalb von einem Rancher Kubernetes Cluster.

Rancher, Kubernetes, check! Ghost installieren?

In diesem Artikel – Kubernetes einfach mit rancher : Ghost CMS – installieren wir gemeinsam Ghost einmal über den Katalog – helm chart – und zum anderen mit einem deployment.

Die Variante mit Helm erstellt 2 pods, einmal für Ghost und einmal für das rdbms (mysql oder maria). Die Standard-Deployment-Variante dahingegen nur einen Pod.

Requirements

Der aufmerksame Leser sollte bis hier alle vorhergehenden 5 Teile gelesen und umgesetzt haben. Wir brauchen einen RKE Cluster, Let´s Encrypt Cert Manager, Storage und Lust 🙂 …

Anstatt einer Storage Lösung wie gluster, OpenEBS, Longhorn, ceph etc. kann natürlich – und sollte meiner Meinung nach auch – managed Speicher von Azure etc. verwendet werden. Wir werden ein Volume brauchen.

Ghost bietet die Möglichkeit Emails zu verschicken um z.B. Team Mitglieder einzuladen. Sofern diese Funktionalität gewünscht ist, sollten die Mail Postfach Anmeldeinformationen vorliegen.

Die Funktionalitäten… Was möchte ich?

Das ist ganz einfach erklärt. Folgende Punkte sind für mich wichtig.

  • Automatische Zertifikats-Anforderung
  • Ausgehende Emails
  • Upload-Größe von 50 mb pro Datei
  • Betrieb mit SQLite

Mit der Bereitstellung über Helm und den darauf folgenden Nacharbeiten, konnte ich die ersten 3 Punkte lösen. Leider war es mir nicht möglich, in dieser Variante, auf die SQL Datenbank zu verzichten. Der Chart hat diese fest eingebunden. Weshalb ich hier eine zweite Variante mit einem Standard-Deployment vorstellen werde.

Ghost in RKE mit Helm

Ghost über Helm bereitzustellen war jedenfalls keine einfache Aufgabe. Nach mehreren Anläufen gelang es mir doch, nur um dann festzustellen, dass ich noch hier und da Anpassungen vornehmen muss.

Nach dieser Erfahrung, habe ich alle wichtigen Einstellungen in einer values.yml Datei festgehalten.

Mit dieser Form der Bereitstellung, stellen wir per Helm Chart einen Pod mit zwei Containern bereit zum einen unsere Ghost Software und zum anderen der Datenbank Container – bei diesem Chart ist es der MariaDB-Container.

Bereitstellung mit Helm

Die Installation kann ganz einfach in Rancher ausgeführt werden. Hierfür reicht es aus, dass der gewiefte Benutzer/Admin in das Vorgesehene Projekt wechselt und durch Auswahl von Apps zu den Katalog-Apps wechselt und auf Launch klickt.

In dieser Ansicht werden alle Charts angezeigt, um die Anzeige einzuschränken kann rechts oben entweder gefiltert oder über die Searchbox die Ergebnismenge eingeschränkt werden.

Wir möchten in die Ghost chart, also tippen wir Ghost in die Searchbox und klicken auf View Details, womit wir in die Chart Maske wechseln.

in dem „Configuration Options“ Abschnitt gibt man den Namen sowie den vorgesehenen Namespace an. Die Chart Einstellungen können entweder als Key-Value in der Maske eingegeben oder per YAML bereitgestellt werden.

In diesem Beispiel verwenden wir eine YAML Datei. Der aufmerksame Benutzer muss die Werte vor dem Anwenden anpassen.

---
  ghostHost: "<blog_url>"
  ghostUsername: "<user>"
  ghostPassword: "<pass>"
  ghostEmail: "<user_mail>"
  ghostBlogTitle: "<blog title>"
  allowEmptyPassword: "no"
  mariadb: 
    rootUser: 
      password: "<mariadb_pass>"
    master:
      persistence:
        enabled: "true"
        storageClass: "<storage_class>"
        accessMode: "ReadWriteOnce"
        size: "<mariadb_größe>Gi"
  persistence: 
    storageClass: "<storage_class>"
    size: "<storage_größe>Gi"
  resources:
    requests:
      memory: "512Mi"
      cpu: "300m"
  ingress: 
    enabled: "true"
    certManager: "true"
    annotations: [{kubernetes.io/ingress.class: "nginx"}, {certmanager.k8s.io/cluster-issuer: "letsencrypt-prod"}]
    hosts: 
    -
      name: "<blog_url>"
      path: "/"
      tls: "true"
      tlsSecret: "<lets_encrypt_cert_secret_name>"

Da ich Beispiele liebe findet sich unten ein Beispiel um die Placeholder beispielhaft zu befüllen.

---
  ghostHost: "ghost.ak8s.de"
  ghostUsername: "admin"
  ghostPassword: "eins2Drei4@Ausrufezeichen"
  ghostEmail: "aytac@kirmizi.online"
  ghostBlogTitle: "smart blog"
  allowEmptyPassword: "no"
  mariadb: 
    rootUser: 
      password: "40302010"
    master:
      persistence:
        enabled: "true"
        storageClass: "longhorn"
        accessMode: "ReadWriteOnce"
        size: "2Gi"
  persistence: 
    storageClass: "longhorn"
    size: "2Gi"
  resources:
    requests:
      memory: "512Mi"
      cpu: "300m"
  ingress: 
    enabled: "true"
    certManager: "true"
    annotations: [{kubernetes.io/ingress.class: "nginx"}, {certmanager.k8s.io/cluster-issuer: "letsencrypt-prod"}]
    hosts: 
    -
      name: "ghost.ak8s.de"
      path: "/"
      tls: "true"
      tlsSecret: "ghost-ak8s-de-crt"

Durch klicken auf „Read from File“ kann nun der Inhalt in die Konsole kopiert werden.

rancher ghost helm chart create form
Die Chart Maske

Durch bestätigen – weiter unten – wird nun die Bereitstellung gestartet.

rancher ghost helm chart deployment view
Nach der erfolgreichen Bereitstellung

Nacharbeiten… Zertifikat und Routing

Da nun ghost per Helm installiert ist, können wir gleich loslegen… Nope, können wir nicht. Der Loadbalancer über helm ist, sagen wir mal, nicht für einen produktiven Einsatz geeignet. Routing funktioniert zwar, aber ohne SSL Verschlüsselung – was heut zu Tage Pflicht ist. Die Stellen an denen wir Hand anlegen müssen, sind nicht viele und kann über die Oberfläche getätigt werden.

Neuer Ingress – layer 7

Die Vorgehensweise für dieses Ergebnis habe ich bereits in einem anderen Beitrag festgehalten, Rancher 2x und Lets Encrypt. Das Verfahren hier ist analog zu den Information in dem verlinkten Beitrag.

Für die Erstellung eines neuen Ingress-Eintrag, wechseln wir in der Oberfläche auf „Load Balancing“ – achtet bitte darauf dass hier im selben Projekt gearbeitet wird. Nachdem wir auf Add Ingress geklickt haben, erwartet uns die Eingabeoberfläche.

Rancher create new Ingress entry.
Neuen Ingress erstelle

Die Eingaben sollte selbsterklärend sein. Tatsächlich gibt es hier nur einen Punkt zu erwähnen. Das vorhandene Target Backend löschen wir durch das klicken auf das Minus-Symbol. Danach klicken wir auf Add Service und wählen den ghost Service aus. Das war es auch schon.

Nachdem wir diesen Ingress abgespeichert haben, müssen wir nun die Zertifikat-Relevanten Einträge setzen, dafür wechseln wir wieder in unseren Ingress – über Edit und erweitern jeweils SSL/TLS Certificates und Labels & Annotations wie im folgenden screenshot.

Rancher ingress Edit Mask
SSL/TLS spezifische Einstellungen

Als nächstes müssen wir das YAML von unserem Ingress bearbeiten und den secretName hinzufügen.

Edit yaml view - ingress
Ingress YAML

Fertig! Somit wären wir mit der Inbetriebnahme über Helm fertig… nicht wirklich.

Die Maximale Upload-Gräße

Spätestens wenn wir ein neues Template für unsere Ghost Umgebung verwenden möchten, laufen wir in diesen Fehler.

Rancher ingress max upload size
Maximale Upload-Größe

Das ist keine Einschränkung von Ghost, sondern kommt von Ingress selber und genau dort müssen wir auch Hand anlegen. Dafür wechseln wir wieder in unsere Load Balancing Ansicht und editieren den Ingress.

Die Upload-Größe beeinflussen wir durch eine weitere Annotation. An dieser Stelle legen wir 50 Megabyte als maximale Upload-Größe fest.

Rancher Ingress increasing max upload size
Ingress maximale Upload-Größe

Natürlich hätte man diese Eingabe auch im Kapitel davor – lets encrypt Zertifikats-Anforderung – tätigen können, wobei die Dramatik dahinter untergegangen wäre. Sind wir nun fertig? Leider nein, die ausgehenden E-Mails funktionieren leider nicht automatisch – auch wenn diese in den Values angegeben wurden. Um diese Funktion zu gewährleisten, müssen wir nun unseren Workload editieren. Hierfür wechseln wir in die Workloads Auflistung und klicken unseren Workload an.

Innerhalb der Workload Detail-Ansicht müssen wir zuerst die drei böbberl anwählen und Edit anklicken.

Rancher edit workload button
Workload bearbeiten

Nachdem die Seite neu geladen hat, können wir unterhalb von „Enviorement Variables“ die benötigten Informationen pflegen. Anschließend bestätigen wir durch das klicken auf Upgrade – weiter unten auf der Seite.

rancher enviroment variables for a workload.
Umgebungsvariablen des Workload

Jetzt sind wir fertig 🙂 …

Ghost in RKE mit Standard Deployment Verfahren

In der von mir verwendeten Helm-Chart ist es nicht möglich, Ghost ohne einen Datenbank Container oder externe Datenbank zu betreiben. Für einen „Ein-Mann-Blog“ reicht die Installation mit SQLite vollkommen aus. Wir sparen uns damit einen Container, ein Volume und eine Software, die gewartet werden muss.

Wenn der interessierte Leser, bereits eine externe MariaDB oder MySQL Installation hosted, sollte eher zu dieser Variante greifen. Alle anderen können weiterlesen.

Vorteile dieser Variante (aus der Sicht von dem Author, also mir)

Einer der Vorteile habe ich bereits genannt, SQLite. Ein weiterer Vorteil ist der Aufwand bei der initialen Bereitstellung. Mit dieser Variante ist dieser viel einfacher. Wir fassen alles in einer YAML zusammen und deployen über die Rancher Oberfläche.

Nachteile?

Diese Variante bringt mit sich auch Nachteile gegenüber der Helm-Chart Variante. Der größte Nachteil an dieser Stelle ist die Update Thematik. Bei der Helm Bereitstellung kann direkt in der Oberfläche das Update gestartet werden.

Das bedeutet nun nicht dass man mit der Deployment Variante sich updates abschminken muss. Nein, der Admin muss sich bei dieser Variante mehr Gedanken machen und ggf. das Update-Verhalten zuerst einmal testen.

Für eine Lab-Umgebung – wie die meine – finde ich das Deployment-Verfahren einfacher.

Inbetriebnahme als Deployment

Ok, dieser Part ist straight forward. Wir haben eine YAML Datei mit der wir alle Einstellungen für die Bereitstellung pflegen. Wir haben keine Nacharbeiten und können direkt fortfahren. Nichts desto trotz greifen wir alle API Konfigurationen im YAML auf und erklären diese. Im Anschluß stelle ich wieder ein Template bereit, mit Placeholdern.

Der Service

Wir brauchen einen Service, mit diesem Service steuern wir die Veröffentlichung unserer Ghost Installation.

apiVersion: v1
kind: Service
metadata:
  name: <service-name>
  labels:
    app: <zielapp-name>
spec:
  ports:
  - name: <port-name>
    port: 2368
    protocol: TCP
    targetPort: 2368
  selector:
    app: ghost
  sessionAffinity: None
  type: ClusterIP

Unser Serivce stellt Ghost über Port 2368 – welcher der Standard Ghost Port ist – bereit. Der Rest sollte selbsterklärend sein.

Der Loadbalancer (Ingress)

Der Load Balancer ist ein Layer 4 ingress und wird mit allen wichtigen Informationen Bereitgestellt – als Beispiel, SSL Zertifizierung, Mxx Upload Size etc..

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    certmanager.k8s.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: 50m
  generation: 4
  name: <ingress-name>
spec:
  rules:
  - host: <hostname>
    http:
      paths:
      - backend:
          serviceName: <service-name>
          servicePort: 2368
        path: /
  tls:
  - hosts:
    - <hostname>
    secretName: <secret-name>

Der Speicher

Ghost braucht Speicher, dieser muss bereitgestellt werden.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: <claim-name>
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: <größe>Gi
  storageClassName: <storage-class-name>

Der Persistent Volume Claim stellt die Funktion für die Erstellung eines dynamischen Volumes bereit. Viel zu berichten gibt es für diesen Abschnitt nicht.

Das Deploymnet

In diesem Deployment geben wir alle benötigten Informationen für eine vollständige Ghost Installation mit.

apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: <deployment-name>
  labels:
    app: <zielapp-name>
spec:
  selector:
    matchLabels:
      app: <zielapp-name>
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: <zielapp-name>
    spec:
      containers:
      - env:
        - name: ALLOW_EMPTY_PASSWORD
          value: "no"
        - name: GHOST_EMAIL
          value: <ghost-user-email>
        - name: GHOST_USERNAME
          value: <ghost-user-name>
        - name: GHOST_HOST
          value: <hostname>
        - name: GHOST_PORT_NUMBER
          value: "80"
        - name: GHOST_PROTOCOL
          value: http
        - name: mail__from
          value: <outgoing-email>
        - name: mail__options__auth__pass
          value: <smtp-pass>
        - name: mail__options__auth__user
          value: <smtp-account>
        - name: mail__options__host
          value: <smtp-host>
        - name: mail__options__port
          value: "<smtp-port>"
        - name: mail__transport
          value: SMTP
        - name: url
          value: <blog-url>
        image: ghost:latest
        imagePullPolicy: Always
        name: <pod-name>
        ports:
        - containerPort: 2368
          name: ghost
          protocol: TCP
        volumeMounts:
        - mountPath: /var/lib/ghost/content
          name: test-ko-ghost-volume
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      terminationGracePeriodSeconds: 30
      volumes:
      - name: <volume-name>
        persistentVolumeClaim:
          claimName: <claim-name>

Unterhalb von spec->containers->env pflegen wir die Umgebungsvariablen inkl. SMTP etc. ein. Zusätzlich verlinken wir hier unseren PVC. Der Rest sollte selbsterklärend sein.

Das Gesamte

Alle vier Definitionen können innerhalb einer Datei gepflegt oder separiert gepflegt werden. In diesem Beispiel verwenden wir eine Datei.

apiVersion: v1
kind: Service
metadata:
  name: <service-name>
  labels:
    app: <zielapp-name>
spec:
  ports:
  - name: <port-name>
    port: 2368
    protocol: TCP
    targetPort: 2368
  selector:
    app: ghost
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    certmanager.k8s.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: 50m
  generation: 4
  name: <ingress-name>
spec:
  rules:
  - host: <hostname>
    http:
      paths:
      - backend:
          serviceName: <service-name>
          servicePort: 2368
        path: /
  tls:
  - hosts:
    - <hostname>
    secretName: <secret-name>
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: <claim-name>
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: <größe>Gi
  storageClassName: <storage-class-name>
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: <deployment-name>
  labels:
    app: <zielapp-name>
spec:
  selector:
    matchLabels:
      app: <zielapp-name>
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: <zielapp-name>
    spec:
      containers:
      - env:
        - name: ALLOW_EMPTY_PASSWORD
          value: "no"
        - name: GHOST_EMAIL
          value: <ghost-user-email>
        - name: GHOST_USERNAME
          value: <ghost-user-name>
        - name: GHOST_HOST
          value: <hostname>
        - name: GHOST_PORT_NUMBER
          value: "80"
        - name: GHOST_PROTOCOL
          value: http
        - name: mail__from
          value: <outgoing-email>
        - name: mail__options__auth__pass
          value: <smtp-pass>
        - name: mail__options__auth__user
          value: <smtp-account>
        - name: mail__options__host
          value: <smtp-host>
        - name: mail__options__port
          value: "<smtp-port>"
        - name: mail__transport
          value: SMTP
        - name: url
          value: <blog-url>
        image: ghost:latest
        imagePullPolicy: Always
        name: <pod-name>
        ports:
        - containerPort: 2368
          name: ghost
          protocol: TCP
        volumeMounts:
        - mountPath: /var/lib/ghost/content
          name: test-ko-ghost-volume
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      terminationGracePeriodSeconds: 30
      volumes:
      - name: <volume-name>
        persistentVolumeClaim:
          claimName: <claim-name>

Ein Beispiel könnte wie folgt aussehen.

apiVersion: v1
kind: Service
metadata:
  name: test-ko-ghost
  labels:
    app: ghost
spec:
  ports:
  - name: ghost
    port: 2368
    protocol: TCP
    targetPort: 2368
  selector:
    app: ghost
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    certmanager.k8s.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: 50m
  generation: 4
  name: test-ko-ghost
spec:
  rules:
  - host: ghost.ak8s.de
    http:
      paths:
      - backend:
          serviceName: test-ko-ghost
          servicePort: 2368
        path: /
  tls:
  - hosts:
    - ghost.ak8s.de
    secretName: ghost-ak8s-de-crt
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-ko-ghost-volume
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: longhorn
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
  name: test-ko-ghost
  labels:
    app: ghost
spec:
  selector:
    matchLabels:
      app: ghost
  strategy:
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: ghost
    spec:
      containers:
      - env:
        - name: ALLOW_EMPTY_PASSWORD
          value: "no"
        - name: GHOST_EMAIL
          value: aytac@kirmizi.online
        - name: GHOST_HOST
          value: ghost.ak8s.de
        - name: GHOST_PORT_NUMBER
          value: "80"
        - name: GHOST_PROTOCOL
          value: http
        - name: GHOST_USERNAME
          value: EinUser
        - name: mail__from
          value: aytac_blog@kirmizi.online
        - name: mail__options__auth__pass
          value: 40302010
        - name: mail__options__auth__user
          value: aytac_blog@kirmizi.online
        - name: mail__options__host
          value: mail.kirmizi.online
        - name: mail__options__port
          value: "587"
        - name: mail__transport
          value: SMTP
        - name: url
          value: https://ghost.ak8s.de
        image: ghost:latest
        imagePullPolicy: Always
        name: test-ko-ghost
        ports:
        - containerPort: 2368
          name: ghost
          protocol: TCP
        volumeMounts:
        - mountPath: /var/lib/ghost/content
          name: test-ko-ghost-volume
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      terminationGracePeriodSeconds: 30
      volumes:
      - name: test-ko-ghost-volume
        persistentVolumeClaim:
          claimName: test-ko-ghost-volume

Die Bereitstellung

Die Bereitstellung ist ganz einfach. In dem Ziel-Projekt navigieren wir in die Workloads Auflistung. an dieser Stelle klicken wir oben auf „Import Yaml“.

rancher import yaml
YAML Importieren

Nun kann man entweder den „Read from a file“ Button oder fügen den Inhalt unserer Deployment Definition in die Konsole.

Zusätzlich muss an dieser Stelle noch der Import Mode eingestellt werden. In meinem Fall findet der Import in das aktuelle Projekt – mein lab Projekt – und dem „Ghost“ Namespace statt.

Sofern wir diese Einstellungen getätigt und auf Import geklickt haben, startet die Bereitstellung.

rancher ghost yaml import
YAML Import

Nach einer erfolgreichen Bereitstellung, sollte nun unsere Ghost Umgebung per HTTPS aufrufbar sein.

rancher ghost deployment
Unser Workload

So, jetzt sind wir fertig! 🙂

Fazit

Ich für meinen Teil habe bei Kubernetes einfach mit rancher : Ghost CMS für die Bereitstellung als Deployment entschieden. Für meinen Einsatzzweck – Spielwiese – ist diese Variante vollkommen ausreichend.

Für einen Produktiven Einsatz empfiehlt sich das Helm-Chart. Egal wie die Bereitstellung einer Produktiv-Umgebung auch sein sollte, ist es empfehlenswert eine dedizierte Datenbank ausserhlab des Clusters zu verwenden. Für die Dateispeicherung von Ghost – Bilder und andere Dateien – können viele Cloud-Speicher Lösungen verwendet werden. Alternativ kann an dieser Stelle mit einer S3 Kompatiblen Storage Class gearbeitet werden. Die Pflege einer Produktiv-Umgebung ist auf jeden Fall kostspieliger.

Mit diesem Artikel beenden wir unsere Kubernetes einfach mit rancher Serie. Ich hoffe dass ich mit dieser Serie anderen Interessenten helfen konnte, meine Anforderung an eine public OneNote sind hiermit erfüllt.

Natürlich werde ich hier weitere Artikel einstellen, ich bin gerade bei der Themen-Findung. Ein Thema sind FaaS (Function as a Service), darüber werde ich den einen oder anderen Beitrag schreiben.

Weitere Themen könnten Backup Strategien, Security etc. werden, dafür bin ich noch am Überlegen und Abwegen.

2 Kommentare

Schreibe einen Kommentar