Teile und Herrsche - das ist in so vielen Aspekten immer wieder wahr.
Nach Jahrmillionen in denen man die Ressourcen eines Computers maximal auslastete, weil die sind ja teuer, begann man - als die Preise fielen, die Aufgaben zu verteilen, ein Server für eine Aufgabe. Dann erkannte man erstaunllicherweise, dass viele der Rechner nicht mal im Ansatz ausgelastet werden. Verschwendung. Dann fing man an, die Dienste in virtuelle Maschinen auf der Hardware zu konsolidieren. Dadurch waren die Rechner gut ausgelastet. Aber die Admins kamen kaum hinterher - jede VM ist ihr eigenes Betriebssystem mit einem Zoo von Diensten und Paketen, damit die VM das tut, wofür sie gedacht ist. Und jedes OS verbrennt auch Ressourcen für OS Aktionen, braucht Sicherheitsupdates und so weiter. Dann ging es los mit Paravirtualisierung und es dauerte dann nicht mehr lange und Docker war geboren.
Docker ist keine Virtualisierung. Auch wenn es manchmal so aussieht, aber es paravirtualisiert, genauer es nutzt die Möglichekten der CPU und des Betriebssystems, die Prozesse von einander zu trennen. Einzig die für einen Prozess notwendigen Ressourcen (Dateien) werden dupliziert und nicht mehr das ganze Betriebssystem. Ich vergleiche das immer gerne mit dem Chroot, was man schon länger kennt.
Das sind schon viele Vorteile, der eigentliche Vorteil ist aber - man verschmutzt mit einem Dockercontainer nicht sein Basissystem. Die Konfigurationsarbeiten für den Betrieb eines Dienstes bleibt im Docker - genauer im Dockercontainer. Wirft man das weg, ist alles spezifische weg. Das hat den Vorteil, dass man schnell was ausprobieren kann, das hat aber auch den Vorteil bei Fehlern schnell zurück zum Grundsystem zu kommen.
Die normale Nutzung von Docker basiert darauf, dass man mit dem Befehl Docker diverse unabhängige Befehle absendet und über Parametrisierung der einzelnen Befehle dann eine Infrastruktur aufbaut. Zum Beispiel braucht es für ein abgeschottetes System aus zwei Diensten Datenbank und Webserver einen Befehl um ein Netzwerk zu erzeugen, einen um den Dienst Datenbank zu starten und mit dem Netzwerk zu verknüpfen und einen um das gleiche mit dem Webserver zu tun. Das ist ziemlich viel Handarbeit und man fängt schnell an, dass zu scripten.
Und hier beginnt die Arbeit von docker-compose
. Dieses Tool bietet die Möglichkeit in einer YAML Datei eine Infrastruktur zu definieren.
Hier mal ein Beispiel:
version: '2.1'
services:
postgres:
image: postgres
volumes:
- ./postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
keycloak:
image: jboss/keycloak
command: -b 0.0.0.0 -Djboss.bind.address.private=127.0.0.1
environment:
DB_VENDOR: POSTGRES
DB_ADDR: postgres
DB_DATABASE: keycloak
DB_USER: keycloak
DB_SCHEMA: public
DB_PASSWORD: password
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: pasword
ports:
- "2600:8443"
depends_on:
- postgres
networks:
default:
driver: bridge
enable_ipv6: true
ipam:
config:
- subnet: "172.26.0.0/24"
- subnet: "fc00:2600::/96"
Das ist ein einfaches Beispiel und es sieht komplizierter aus, als es ist. Die erste Zeile enthält eine Versionsnummer, die für docker-compose relevant ist. Hier ist 2.1 eine gute Wahl. Es gibt auch neuere Versionen, die bringen aber nur etwas in Zusammenhang mit docker-swarm, welches wir nicht nutzen. Dann gibt es drei Blöcke (in unserem einfachen Beispiel sind nur zwei zu sehen) - services, networks, volumes.
Im Bereich services
definieren wir, die jeweiligen Docker-Container, die zusammen einen “Service” abbilden. Theoretisch kann man hier eine ganze Infrastruktur abbilden - allerdings begrenz docker-compose sich auf einen Host und verteilt die Container nicht in einem Kubernetes Cluster. Jedes Element darunter hat einen frei zu wählenden Namen und darunter gibt es dann einige elementare Einträge wie image
oder build
um anzugeben, welcher Container zu verwenden ist, welche Ports freigegeben werden bestimmt man mit ports
. Der Rest ist optional. Zum Beispiel environment
definiert Umgebungsspezifische Variablen. Man kann diese auch aus einer Datei auslesen. Man kann für bestimmte Pfade im Container auch volumes
deklarieren, um sie außerhalb des Containers zu lagern. Hier verwende ich einen relativen Pfad - was in Docker eigentlich nicht erlaubt ist, aber mit docker-compose ab seiner Definitionsdatei funktioniert. Man kann auch Volumes definieren, die anders liegen. Dafür gibt es ja die Volumes Definition (die wir hier nicht verwenden).
Weiterhin kann man Netzwerke mit networks
definieren - ich mache das gerne explizit, weil man so mehr Kontrolle hat und für IPv6 ist das sogar zwingend notwendig, weil Docker bzgl. IPv6 leider kein gutes Vorbild in der Umsetzung ist. Es wird gesagt, IPv6 geht - man muss es aktivieren (via enable_ipv6), aber es wählt dann nicht selbständig ein Segment aus dem Host-Adressraum, es macht kein NAT (und arbeitet damit bei IPv6 gänzlich anders als IPv4). Viel schlimmer ist aber, dass offene Ports von Containern in IPv6 für alle offen sind, egal ob man das will oder nicht. Bei IPv4 wird die ports
deklaration also beachtet - bei IPv6 nicht. Ich verwende daher gerne ein Hilfspaket von robertkl/ipv6nat.
Zum Thema IPv6 NAT kann man viel erzählen und diskutieren. Aber so lange man keine dynamische Kontrolle über die Ports hat, die Docker aufmacht, ist das Thema bei einem direkt ans Internet gehängten Server obsolet. Da muss was “vor”.
Bedingt durch die Unabhängigkeit der einzelnen Services voneinander und der Tatsache, dass jedes seine eigene Infrastruktur bekommt, ergibt sich in einer gemeinsam zu nutzenden Ressource schnell ein Problem. Bei Docker ist das das Netzwerk. Jeder Service bekommt sein eigenes lokales internes Netzsegment und das landet alles auf einem Interface. Zum Teil schränkt Docker die Nutzung erheblich ein. Zum Teil ignoriert es bestehende Regeln und liefert keine Lösung. Dann muss die Firewall ran.
Wir haben auf einem Server A einen Service etabliert, und wollen nun nur von einem Server B darauf zugreifen. Alle anderen sollen den Service nicht sehen. Hängt der Server im Internet, kann man das nur mit lokalen Firewallregeln auf Server A direkt lösen.
Docker erstellt selber ein kleines Regelwerk in den IPTables Tabellen NAT und Filter. Dabei geht es Docker primär darum, die Pakete vom externen Netzwerkinterface auf das interne Netz des jeweiligen Service zu bekommen.
Die NAT Regeln sorgen dafür, dass Anfragen vom externen Interface auf dem externen Port auf das interne Interface des Docker Containers mit seinem internen Port weitergereicht werden. Hier sollte man nicht viel dran ändern. Bei den Filtern kann man aber sowohl INPUT als auch eine Docker-spezifische Chain DOCKER-USER befüllen. Wichtig dabei ist - wenn, dann nur diese beide Chains flushn und alles andere so lassen.
Was ich mache ist - die INPUT Chain wie üblich konfigurieren - alles was rein darf, darf rein, den Rest drop’n. Zu beachten gilt, dass die Forward Chain für NAT zuerst angesprochen wird und INPUT deshalb nur für Nicht-Docker Dienste relevant ist. Die für Docker relevanten Services finden sich in DOCKER-USER Anwendung und müssen dort getrennt behandelt werden. Bei der DOCKER-USER Chain (die in FORWARD angesiedelt ist), muss man vorsichtig sein mit dem DROP oder REJECT. Hier sollte man die Regeln nur auf das externe Interface anwenden, damit die Kommunikation zwischen Containern (die über interne Bridges und docker0 laufen) nicht gestört werden.
In letzter Zeit kam das Thema Cloud häufiger in meiner Umgebung vor, als Virus oder Bit.
Wenn man dann erstmal anfängt, darüber nachzudenken, was das Wort bedeutet, kommt man schnell auf die Frage, wie ist Cloud eigentlich definiert. Und da fängt es schnell an, wolkig zu werden. Es gibt zwar Definitionen, aber die sind schwammig und bedeuten alles und nichts. Die einen kennen eine Private Cloud und meinen damit zumeist eine Umgebung mit Hosts und VMs. Die andere Seite spricht gerne auch mal von “Cloud-nativ” und meint damit Serverless-Systeme, bei denen jeder Request einen Script ausführt und dessen Ausführungszeit haargenau abgerechnet wird. Was eine Cloud ist - ist also beliebig wolkig zu sehen.
Ich bin nicht wirklich glücklich damit, zumal mich das Marketingdeutsch gewaltig stört. Mein Auto hat ‘ne Batterie. Ist es deshalb nicht auch ein E-Auto?…
Taucht man etwas tiefer in die Welt der Cloud-afinen Menschen ein, beginnt sich der Wald etwas zu lichten. In der Realität muss man nämlich konkreter werden und da geht es dann schnell um die Themen, die wir auch einfach beim richtigen Namen nennen können. Nur sind die nicht so griffig. Ziel ist es, die Services maximal von einander zu trennen. Beim Thema Host+VMs handelt es sich um Virtualisierung. Hier kann man relativ einfach pro VM einen Service definieren. Der Vorteil ist. Der Service hat seine gesamte Landschaft, die ihm behagt, um sich herum gestrickt. Der Nachteil ist, dass der gesamte Overhead eines kompletten Betriebssystem jedesmal mitkommt. So wird aus einem simplen Java-Prozess gleich ein Thema um Kernel Updates, Firewalls und ich weiß nicht was.
Da der Overhead für große Umgebungen irgendwann unwirtschaftlich war und die Linuxwelt schon lange eine Alternative anbot, entwickelte sich mit der Zeit das Thema Paravirtualisierung zu einer besseren Lösung. Als Docker Inc. dann noch mit verhältnismäßig simplen Tools für CLI um die Ecke kam, war das Thema Cloud langsam geboren und benannt. Paravirtualisierung trennt die Services durch Prozessisolation. Ein Programm A kann in seiner Isolationsschicht nicht auf Programm B zugreifen, wenn dieser sich (leienhaft ausgedrückt) nicht im gleichen Context befindet. Was also eine CPU bei der Virtualisierung mit VMs macht, kann auch der Linux-Kernel auf Prozessebene. Er trennt die Programme hart voneinander. Durch den Einsatz von Images (eigentlich TAR-Balls) kann jeder Service in einen Container verpackt werden und liefert sein Ökosystem mit (also alle Libs, die ein Programm so während der Laufzeit braucht). Das OS wird aber nicht wirklich gestartet. Nur das Programm. Nun ist <a href="Docker ansich schon schön - aber auch hier gilt wieder das Problem - je mehr Services man betreibt, desto unübersichtlicher wird es.
Also fingen einige Leute an, ein System um Docker zu stricken. Sie nutzen die Ausführungsmöglichkeiten und die Tools um eine Management Umgebung drumherum zu bauen und fertig war Kubernetes.
Gefühlt ist die Installation von Kubernetes einfach… wenn man weiß, wie. In der Realität gibt es diverse Haken und Ösen. Benutzt man Docker als Unterbau, ist das Leben relativ einfach, weil sich das Ökosystem Kubernetes um Docker aufbaut. Da Docker aber langsam stirbt und die diversen Probleme ziemlich nerven, würde man gerne auf was anderes setzen. Mein erster Gedanke war Podman
. Leider braucht Kubernetes einen Dienst, gegen den er sich verbinden kann und da kam Podman anfangs nicht in Frage. Deshalb hab ich die Alternative Basis CRI-O
eingesetzt. Letztlich wrappen Docker und Podman CRI-O. Da Docker nun auch schon langsam sinkt, bedient man sich dem daraus geschaffenen Industriestandard containerd
.
Ich hab zwei Wege ausprobiert, um Kubernetes zu installieren.
Der Ansible Script, den ich gebaut habe, tut eigentlich nicht viel mehr als folgendes:
Auf enem dem Master-Node wird dann mit kubeadm init ..
der Cluster gestartet und das Netzwerk vorbereitet. Die einzelnen Nodes joinen dann den Cluster mit kubeadmin join
und einem Token.
Dieses Basissetup ist dann erstmal ein Cluster. Es gibt eine Reihe von Services, die dem kube-system hinzugefügt werden sollten.
Ich gebe zu, Kubernetes alleine ist im produktiven Einsatz schwierig zu handhaben. Der Umgang mit den Yaml-Configs, die man “apply”‘n muss, ist gewöhnungsbedürftig. Deshalb kamen später auch Managementtools, wie OpenShift auf, um einem Admin das Leben in produktiven Umgebungen zu erleichtern.
Da ich sowieso überall und nirgends Ansible verwende, liefert eine Suche im Netz relativ schnell kubespray
ans Tageslicht. Kubespray ist ein spezielles Repository mit einem eigenen Ökosystem für Kubernetes. Es richtet nicht nur Kubernetes an sich ein, es bereitet die Systeme auch entsprechend vor. Leider lässt es sich schlecht in eigene Ansible Repositories integrieren. Das ist aber auch so ziemlich der einzige Nachteil.
Folgende Schritte sind zu erledigen.
cp -r inventory/sample inventory/mycluster
kube_nodes
, drei etcd
Nodes und ein oder zwei kube_control_plane
setcd_deployment_type
auf host
stellen (statt docker)container_manager
auf containerd
stellenAnschließend dann das Playbook cluster.yml
ausführen. Es wird ein etcd installiert. Dann wird der ausgewählte container manager ausgeführt (in unserem Fall containerd). Dann wird Kubernetes konfiguriert
Anschließend kann man das Kubernetes Dashboard installieren.
Die Konfigdatei mit curl https://raw.githubusercontent.com/kubernetes/dashboard/master/aio/deploy/recommended.yaml > kubernetes-dashboard-deployment.yml
herunterladen. Die Datei enthält diverse fürs Dashboard notwendige Deklarationen. Eine davon ist der Service kubernetes-dashboard
. An diesem stellt man den spec-type Nodeport ein.
...
kind: Service
apiVersion: v1
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kubernetes-dashboard
spec:
ports:
- port: 443
targetPort: 8443
selector:
k8s-app: kubernetes-dashboard
type: NodePort
...
Der Sinn dahinter ist, dass damit das Dashboard auf allen Nodes im Kubernetes auf einen Port erreichbar ist und keinen Ingress braucht. Die Datei dann mit kubectl apply -f kubernetes-dashboard-deployment.yml
einfügen.
Anschließend muss ein Admin-Account angelegt werden. Das ist etwas seltsam nur mit Tokens.
# acoby-admin.yml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: acoby-admin
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: acoby-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: acoby-admin
namespace: kube-system
Das auch einbinden mit kubectl apply -f acoby-admin.yml
. Dann das Token extrahieren.
SA_NAME="acoby-admin"
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep ${SA_NAME} | awk '{print $1}')
Denn relevanten Ports auf den Nodes findet man mit
kubectl get service -n kubernetes-dashboard
Dann brauchen wir einen Ingress Controller. Den gibt es hier
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.43.0/deploy/static/provider/baremetal/deploy.yaml
Ein Beispiel Service auf Basis von HTTPBin konfiguriert man wie folgt.
kubectl apply -f https://github.com/istio/istio/raw/master/samples/httpbin/httpbin.yaml
Damit ist der Service im Kubernetes ausgerollt. Um ihn über den Ingress Controller erreichbar zu machen, muss folgende Definition eingefügt werden.
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: httpbin-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: httpbin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: httpbin
port:
number: 8000
Relevant ist hier der Host und sein Name. Über die Domain httpbin.example.com
werden alle Anfragen an den Ingress an den Service httpbin auf Port 8000 weitergeleitet.
Hier ein paar hilfreiche Kommandos
kubectl get ing
kubectl get svc -n ingress-nginx
Der letzte Befehl zeigt uns den Quellport für unseren Kubernetes Ingress. Dieser ist von außen erreichbar.
Am Ende noch ein Beispiel für NFS. Die Einbindung für persistente Volumes ist trivial.
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs
spec:
capacity:
storage: 200Gi
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Recycle
storageClassName: nfs
nfs:
path: /mnt/md1/shares/kubernetes
server: 192.168.10.2
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
labels:
demo: nfs-pvc
spec:
storageClassName: nfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 200Gi
Dabei wird das NFS Share /mnt/md1/shares/kubernetes
vom NFS Server 192.168.10.2
mit 200GB als PersistentVolume und Claim dem Cluster bekannt gemacht.
Hier eine Liste nützlicher Befehle
kubectl cluster-info
Zeigt einem den Status des Kubernetes Clusters an. Wichtig finde ich den Befehl, weil er den Einstiegspunkt (Master) aufzeigt.
kubectl completion bash > /etc/bash_completion.d/kubectl
Erzeugt die Bash-Completion und erstellt eine entsprechende Datei für Bash (gibts auch für zsh). Danach einmal neu einloggen und das Leben mit kubectl
ist deutlich einfacher.
kubectl get services --all-namespaces
Die Services in Kubernetes sind in Namespaces gruppiert. Mit diesem Befehl kann man sich alle Namespaces und die darin definierten Services anzeigen lassen. Sehr wichtig für den ersten Überblick.
kubectl get pods --all-namespaces
Mindestens genauso interessant ist die Liste aller Pods. In jedem Namespace, zu jedem Service gibt es eine Reihe von Pods. Jeder Pod enthält einen oder mehreren Container (also Docker-Prozess).
kubectl get secrets
Liefert eine Liste aller Tokens, die in dem Cluster im Namespace default konfiguriert sind. Kann man mit –all-namespace auch auf alle Namespaces anwenden.
kubectl get secrets default-token-....
Liefert dem Default-Token. Eigentlich das gleiche, wie get secret. Mit folgendem Befehl kann man sich aber das Zertifikat ziehen, was man zB. braucht, um Gitlab zu füttern.
kubectl get secret default-token-... -o jsonpath="{['data']['ca\.crt']}" | base64 --decode
Liefert das Zertifkat des Defaulttokens. Das wird zB. für Gitlab gebraucht.
kubectl apply -f patch.yaml
Mit dem Apply Parameter kann man den Kubernetes Cluster konfigurieren. Das ist ein hochgradig wichtiger Befehl, der so richtig nervig ist. Denn die Daten im YAML Format sind alles andere als hübsch und schlecht beschrieben. Beispiele folgen weiter unten.
kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep gitlab-admin | awk '{print $1}')
Damit bekommt man eine Token für den externen Zugriff aufs System. Der innere Kubectl Befehl listet alle Tokens auf, grepped den User bzw. Tokenname, den man will und der zweite äußere Befehl zeigt mit describe secret die Daten darin an.
kubectl logs --namespace gitlab-managed-apps install-helm
Damit kann man sich die Logs eines Pods anschauen.
kubectl exec -it --namespace default shell-demo /bin/bash
Mit dem Befehl kann man in einen Pod einsteigen und einen Befehl ausführen. Das -it verursacht, dass der Befehl nicht gleich wieder beendet wird (also wirklich ein TTY öffnet). Ist sehr ähnlich zu docker/podman exec.
heml repo add stable https://kubernetes-charts.storage.googleapis.com
helm repo update
helm repo remove stable
Helm ist eine Art Meta-CLI-Befehl. Damit kann man in den Cluster einige vorgefertigte Pakete installieren. Ziel ist die Vereinfachung der Installation und des Managements. Ist wie ein Paketmanager zu betrachten. Die Pakete liegen als Charts vor. Meiner Meinung nach ein verwirrender Begriff.
Hier finden sich einige nützliche und interessante Links an, die sich im Laufe der Recherche angesammelt haben.
Mal genauer reinschauen:
bootcamp2.yaml
apiVersion: v1 kind: Service metadata: name: bootcamp-service spec: type: ClusterIP selector: app: bootcamp ports:
04 helm repo list 07 helm repo update 20 kubectl get pods 64 kubectl get pods -o wide 22 kubectl get nodes 30 kubectl get deployments 56 kubectl get services 57 kubectl get svc 26 kubectl proxy 44 kubectl describe pods 33 kubectl apply -f bootcamp.yml 42 kubectl exec kubernetes-bootcamp-fcc5bfb48-48pvr - sh 52 curl http://localhost:8001/api 53 curl http://localhost:8001/api/v1/namespaces/default/pods/kubernetes-bootcamp-fcc5bfb48-48pvr 54 curl http://localhost:8001/api/v1/namespaces/default/pods/kubernetes-bootcamp-fcc5bfb48-48pvr/proxy 55 curl http://localhost:8001/api/v1/namespaces/default/pods/kubernetes-bootcamp-fcc5bfb48-48pvr/proxy/ 65 more /run/flannel/subnet.env
120 kubectl get services my-nginx 121 kubectl delete services my-nginx 122 kubectl delete services hellok8s-service 123 kubectl delete services kubernetes 124 pico test.yaml 125 kubectl delete deployments.apps hellok8s-deployment 126 kubectl create -f test.yaml 127 kubectl get all 128 kubectl get all 129 kubectl get all 130 kubectl port-forward service/hellok8s-service 8080:8080 131 more test.yaml 132 kubectl get all 133 kubectl port-forward service/bootcamp-service 8080:8080 134 ll 135 ls -al 136 curl https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml 137 curl https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml 138 kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/namespace.yaml 139 kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.3/manifests/metallb.yaml 140 kubectl create secret generic -n metallb-system memberlist –from-literal=secretkey="$(openssl rand -base64 128)" 141 kubectl get all -n metallb-system 142 kubectl get all -n metallb-system 143 pico metallbconfig.yaml 144 kubectl apply -f metallbconfig.yaml 145 ls -al 146 more bootcamp2.yml 147 history
für die dauerhafte Verwendung von Wireguard auf einem kompletten Netzwerk ist OpenWRT auf einer separaten Fritzbox eine gute Wahl.
Was haben wir gemacht.
Auf einem Notebook das Projekt herunterladen (eigentlich brauchen wir nur einen Script)
Von https://openwrt.org/toh/hwdata/avm/avm_fritz_box_7412 das Initramfs Image und Squashfs Image herunterladen
Die Fritzbox 7412 per Netzwerk ans Notebook andocken (DHCP an)
Die Fritzbox anschalten und möglichst sofort den folgenden Script starten, weil der Telnet-Port nur während des Boot-Prozesses offen ist
./scripts/flashing/eva_ramboot 192.168.178.1 openwrt-19.07.1-lantiq-xrx200-avm_fritz7412-initramfs-kernel.bin
Nach kurzer Zeit ist die Fritzbox dann per SSH erreichbar.
scp openwrt-19.07.1-lantiq-xrx200-avm_fritz7412-squashfs-sysupgrade.bin root@192.168.1.1:/tmp
ssh root@192.168.1.1
openwrt$> cd /tmp
openwrt$> sysupgrade -n openwrt-19.07.1-lantiq-xrx200-avm_fritz7412-squashfs-sysupgrade.bin
Anschließend wird man ausgeloggt und OpenWRT wird auf der Fritzbox installiert. Anschließend loggt man sich noch mal ein und setzt ein neues root-Kennwort.
ssh root@192.168.1.1
openwrt$> passwd
Nun kann man sich auf der Box via Browser einloggen.
Ceph ist ein verteilter Objektspeicher für Linux.
Ceph benötigt im Gegensatz zu Gluster ganze Platten. Theoretisch könnte man wohl auch Partitionen angeben, das ist aber ein ziemlicher Hack und deshalb lassen wir das lieber.
Objektspeicher bedeutet, dass die Festplatten nicht mit einem klassischen Dateisystem arbeiten, sondern mit einer Datenbank und einem Index. Laienhaft werden dabei die Objekte als Dateien oder Dateiblöcke betrachtet, in der Datenbank abgelegt.
Auf den Proxmox Hosts ist Ceph bereits in Vorbereitung vorhanden. Als Grundvoraussetzung gilt, dass die Proxmox
in einem Cluster verbunden sind und sich gegenseitig kennen. Das sieht man daran, dass die Config im Corosync
Ordner unter /etc/pve
angelegt wird. Dieser wird in einem Proxmox Cluster über alle Knoten immer synchron gehalten.
Für jeden Host muss man
pveceph install
ausführen. Damit wird das Proxmox Ceph Debian Repository konfiguriert und die Ceph Pakete installiert.
Im Folgenden muss man Ceph initialisieren. Das geht mit
pveceph init -network 10.255.255.0/24 -cluster-network 10.255.255.0/24
Dies muss man auf dem ersten Host machen. Dadurch wird unter /etc/pve/ceph.conf
eine Konfiguration
im Corosync System angelegt, auf die in /etc/ceph/ceph.conf
ein Link erzeugt wird.
Auf allen anderen Nodes reicht ein
pveceph init
Wichtig ist noch der Keyring, der auch mit dem Init erzeugt wird und auf allen Nodes der gleiche sein muss.
Pro Proxmox Host kann man dann noch einen Monitor erzeugen, mit:
pveceph createmon
Wobei das veraltet scheint. Die Doku spricht davon, aber in der CLI heißt es pveceph mon create
.
Führt man den Befehl aus, wird auf dem dazugehörigen System ein Dienst gestartet, der auf Port 6789 horcht.
Analog sollte man auch mindestens einen Manager anlegen mit pveceph mgr create
. Auf dem ersten Knoten, bei dem ein Monitor angelegt wird, wird automatisch auch ein Manager angelegt. Alle weiteren sind dann nur im Standby relevant.
Erkenntnis - bei Proxmox/Ceph kann man nur ganze Platten hinzufügen. Das ist schwierig, wenn das Basissystem nur aus einem RAID-1 besteht, auf dem auch das OS liegt. Im Falle einer NVMe plus 2 SATA könnte man Ceph so aufbauen, dass es über die SATA Platten läuft.
Hier finden sich einige nützliche und interessante Links an, die sich im Laufe der Recherche angesammelt haben.
Manchmal merkt man es gar nicht.
Am Anfang braucht man irgendwo irgendeine Linuxumgebung, um ein paar Dienste zu betreiben und dann noch einen und noch einen und dann reicht eine Instanz pro Dienst nicht mehr aus und dann sind es plötzlich zwei oder drei und man braucht ein Monitoring und dann ist es passiert. Man hat den einen Service irgendwo vergessen, der nachts sang und klaglos abgeschmiert ist oder schlimmer, jemand findet eine Sicherheitslücke oder Kunden wundern sich, weil ihre Dienste nicht mehr erreichbar sind.
Es geht so schnell. Technische Infrastrukturen haben die Angewohnheit schnell richtig komplex zu werden. Selbst ein einfaches Szenario mit einem Root-Server und ein paar VMs oder Docker Diensten darauf und schwubs ist die Firewall mal nicht richtig konfiguriert, wenn man IPv6 nutzen will. Man ist nur noch am Switchen zwischen den Konsolen, testet eine Konfiguration hier und überträgt sie auf die anderen Umgebungen und zwei Tage später weiß man nicht mehr genau, was man gemacht hat.
Kommt das bekannt vor? Mir ja - ich hab das bestimmt schon dutzende Male erlebt und jedesmal sag ich mir - beim nächsten Mal baue ich gleich einen Script. Oder besser noch - ich nutze irgendein Tool, dass das für mich macht.
Und dann kam Ansible.
Es gibt noch einige andere Alternativen zu Ansible - ich will hier aber keinen Vergleich machen, denn auch die haben ihre Daseinsberechtigung und am Ende belebt Konkurrenz das Geschäft - oder zumindest profitieren alle Tools von guten Ideen der anderen Tools. Ich hab mich für Ansible entschieden, weil es im Alphabet ganz vorne steht. Dagegen kann man nichts sagen.
Ich nutze Ansible, weil man absolut dezentral arbeiten kann. Es braucht keine zentrale Kommandozentrale (auch wenn es sie gibt), man braucht nur einen Python-fähigen Rechner. Und das wars.
Nun ist Python sicher nicht die Beste aller Programmiersprachen. Das Deisngkonzept mit festen Einrückungen erhöht die Lesbarkeit - aber auch die Fehleranfälligkeit. Zudem ist der unglückliche Bruch zwischen Python 2 und Python 3 eine Herausforderung. Diese zu meistern, wird in den kommenden 1-2 Jahren sicher sehr interessant, denn Python 2 wird 2019 eingestellt. Und man glaubt gar nicht, wie viele Scripte für Ansible dann doch nicht wirklich Python3 kompatibel sind, obwohl sie es sein sollten. Die Reihe von Warnings im Laufe meiner Tests mit fremden Rollen sagt mir aber, dass da noch einiges an Arbeit zu erledigen ist und Python2 noch lange gebraucht wird.
Aber wir sind schon sehr tief in den Details. Was ist Ansible. Ansible ist eigentlich nur eine Hilfe zur Selbsthilfe. Es ist ein Sammelsorium von Tools für die Verwaltung von Unix-basierten Rechnern. Ich beschränke mich jetzt hier konkret auf Debian - aber es läuft eigentlich überall und man kann auch ziemlich gut Scripte für alle Umgebungen bauen.
Dabei bietet Ansible ein kleines Universum von Tools zur Verwaltung fast aller Dienste, die es so gibt und wenn doch nicht, kann man sie recht einfach selber bauen und erweitern. Das Prinzip ist grundsätzlich einfach zu beschreiben. Die meisten Arbeiten bei der Verwaltung von Systemen besteht darin, Programme mit bestimmten Parametern zu starten oder zu beenden und Konfigurationsdateien aller Art verändern oder zu verschieben. Das macht man jeden Tag und hier helfen Tools wie Ansible. Das Problem ist nämlich, wenn man es selber macht, dass man es idempotent macht - sprich, wenn man ein Programm gestartet hat und es läuft, muss man es nicht noch mal starten. Hat man eine Datei verändert, muss man die Datei nicht noch mal ändern. Die Lösung für diese Herausforderung unterstützt Ansible sehr gut - es gibt aber immer noch Restarbeiten, die von einem selber gelöst werden müssen.
Ansible kann man eigentlich überall installieren. Die Rechner, die damit verwaltet werden sollen, brauchen keine spezielle Installation. Aber irgendwo muss man es ausführen. Und das ist in erster Linie der eigene Rechner oder je nach Sicherheitsanforderung ein speziell preparierter Server. Dazu später mehr.
Wir sind ja alle cool, deshalb bauen wir im folgenden Beispiel auf MacOS. Ist aber eigentlich egal - denn wir nutzen einen Terminal und einen Editor nicht viel mehr.
curl https://bootstrap.pypa.io/get-pip.py | sudo python3
sudo pip3 install ansible netaddr passlib
Die erste Zeile installiert via Python3 den Python Paketmanager pip
. Die zweite Zeile installiert via pip dann die Python Pakete von Ansible und ein paar Hilfsbibliotheken (dazu später mehr). In Zukunft wird es vermutlich gar kein Python mehr standardmäßig auf MacOS geben, zumindest hat Apple das angekündigt, aber bis dahin schließen andere Paketmanager wie homebrew die Lücke.
Anschließend müsste durch Eingabe des Befehls
ansible --version
eine Ausgabe folgender Art erfolgen
ansible 2.8.0
config file = None
configured module search path = ['/Users/me/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/local/lib/python3.7/site-packages/ansible
executable location = /usr/local/bin/ansible
python version = 3.7.3 (default, Mar 27 2019, 09:23:15) [Clang 10.0.1 (clang-1001.0.46.3)]
Das war bisher sehr einfach. Jetzt beginnt der Spaß. Bevor wir richtig in die Vollen gehen, eine kleine Randnotiz, wie Ansible arbeitet.
Ansible arbeitet Datei-basiert. Es erwartet 1-n Dateien an ganz konkreten Plätzen. Diese Plätze kann man ändern oder konkret benennen - denn die Defaultwerte sind eher unüblich - ich verstehe zum Beispiel nicht, warum man per Default als nicht priviligierter User im Verzeichns /etc/ Dateien ablegen sollte, die dann der User-space-Prozess nutzt und vor allem Dateien wie /etc/hosts dafür mißbrauchen sollte, Ansible zu konfigurieren. Aber dafür gibt es Best-Practice Beispiele.
Also - wenn man Ansible startet - und das tut man mit einigen Terminalbefehlen, dann liest Ansible mindestens eine Datei (das Inventory) und mindestens eine Aufgabendatei (Playbook). Im Inventory steht, welche Rechner für Ansible relevant sind und wie es diese erreicht. Im Playbook stehen die eingangs erwähnten Befehle, die man auf allen gleichartigen Rechnern ausführen will. Ansible loggt sich dafür in jeden Rechner via SSH ein und führt auf diesem dann den Befehl aus. Das ist sehr einfach ausgedrückt, aber entspricht in etwa dem, was man zu erwarten hat.
Nun wäre es einfach, selber einen Shellscript zu schreiben und ihn via SSH einfach auf allen Remote-Rechnerns auszuführen - aber Ansible kann mehr. Führt man den Script nämlich einmal aus - wird alles im Script wie erwartet abgearbeitet. Interessant wird die zweite Ausführung. Denn Ansible ist so gebaut, dass man einen bestehenden Script immer mehr erweitert und nicht jedesmal einen neuen baut - die idempotente Arbeitsweise verhindert, dass Dinge mehrfach ausgeführt werden, die lieber nicht mehrfach ausgeführt werden sollen. Auf diese Weise hat man einen Ort, an dem man schauen muss und an den man Änderungen machen muss und damit beginnt es einfach und kompliziert zugleich zu werden.
Einfach - weil halt nur ein Playbook für alles - kompliziert, weil ein Webserver und ein Datenbankserver halt andere Pakete brauchen. Man muss sie trennen und dafür nutzt man das Inventory File. Dort definiert man die Rechner (Hosts) und teilt sie in Gruppen ein. Diese Gruppeninformation nutzt man im Playbook, um bestimmte Rollen auf bestimmte Gruppen anzuwenden.
Kommen wir zurück zur Konfiguration. Ein einfaches Beispiel für eine Inventory Datei ist:
server1 ansible_host=1.2.4.8 ansible_port=22 ansible_user=root
server2 ansible_host=1.2.4.16 ansible_port=22 ansible_user=root
[web]
server1
[db]
server2
[ssh]
server1
server2
Dies ist eine Inventory Datei im INI-Format. Für kleine Umgebungen empfehle ich dieses Format, weil man schnell einen Überblick bekommt und schnell Sachen ändern kann. Wenn die Inventory Datei säter mehr Informationen enthält, dann macht es Sinn, ein YAML Format zu wählen, ist die Umgebung hochgradig variable (Cloud-Maschinen) macht es Sinn sich über dynamische Inventories Gedanken zu machen.
Ein YAML Format für das Inventory ist dann empfehlenswert, wenn man sich langsam im Übergang von einfachen in komplexere Umgebungen befindet und diverse Variablen pro Host oder Gruppe konfigurieren will.
all:
hosts:
server1:
ansible_host: 1.2.4.8
server2:
ansible_host: 1.2.4.16
vars:
ansible_port: 22
ansible_user: root
children:
web:
hosts:
server1:
db:
hosts:
server2:
vars:
customer_variable: value
ssh:
hosts:
server1:
server2:
Ist auch hübsch. Der einzige Unterschied zur obigen INI Datei ist die Variable customer_variable, die man im Fall des INI Formats in einer bestimmten Datei ablegt und Ansible diese importiert. Das ist am Anfang aber jetzt zu kompliziert.
Ein Playbook kann so aussehen.
- name: Install Docker on VMs
hosts: vms
roles:
- common
- docker
Hier wird im YAML Format geschrieben. Das ist die übliche Sprache in Ansible, um Playbooks zu schreiben. Konkret wird einer Gruppe vms die Rollen common und docker zugewiesen. Die in diesen Rollen abgelegten Befehle werden damit ausgeführt. (man sieht schon - man kann das hinreichend komplex verschachteln). Alles mit einem Bindestrich ist ein Task. Ihnen gibt man mit name
einen Namen. Dann grenzt man mit hosts
die Gruppe ein. Mit roles
werden über Unterpunkte (Bindestriche ist hier eine Liste). Man kann das sehr gut mit Markdown Listen vergleichen.
An dieser Stelle eine weitere Einschränkung zur Aussage, dass Ansible keine Bedingungen an die Umgebung hat. Man muss zwar keinen zusätzlichen Agent auf den Rechnern laufen lassen - und sich damit weiter herumschlagen, wie man den Port dicht macht und das updatet. Aber man braucht zwingend SSH und damit Ansible richtig rockt - muss auf allen Rechnern Python installiert sein. Dies tue ich im Bootstrap in einer ROlle namens common immer mit.
- name: Bootstrap all hosts
hosts: all
gather_facts: no
pre_tasks:
- name: install python 3
raw: test -e {{ ansible_python_interpreter }} || (apt -y update && apt install -y python3 python3-minimal python3-pip python3-dev python3-apt)
register: output
changed_when: output.stdout != ""
- setup: # aka gather facts
roles:
- common
- geerlingguy.ntp
In diesem Task kommen mehrere Dinge zusammen. Er gilt für alle Hosts. gather_facts: no
weißt Ansible an, beim Aufbau der SSH Verbindung keine Informationen über den Zielhost zu sammeln. Normalerweise tut Ansible dies automatisch bei jeder Ausführung. Und nutzt dafür Python auf dem Zielhost - nur, wenn es nicht da ist, kommt es zu einem Fehler - also keine Fakten sammeln.
Dann wird vor allem anderen ein pre_task
ausgeführt. Und zwar die Installation von Python. Da wir gleich up2date sein wollen, installieren wir gleich Python3. Hier kommt dann wieder die Eigenschaft von Ansible zum Tragen. Ansible möchte Idempotent sein. Das bedeutet leihenhaft - wenn ein Zustand bereits erreicht ist, muss man nicht noch mal alles machen, der Zustand ist erreicht. Dafür dient changed_when
. Das register
speichert die Ausgabe des Installationsprozesses. Am Ende des Pretasks wird dann setup
ausgeführt. Das ist dann nichts anderes als gather_facts: yes
. Man kann das alles weglassen - muss dann aber immer daran denken, wenn man einen neuen Rechner anlegt, dass dieser Python braucht - das ist wieder handische Arbeit - und das wollen ja vermeiden.
Das obige Beispiel mit den Rollen ist jetzt wenig hilfreich. Würde man das ausführen, klappt es nicht, weil in dem Szenario mit zwei Dateien, kennt Ansible keine Rolle common oder docker oder geerlingguy.ntp. Ein Standalone Playbook wäre also eher sowas:
- name: Bootstrap all hosts
hosts: all
gather_facts: no
pre_tasks:
- name: install python 3
raw: test -e {{ ansible_python_interpreter }} || (apt -y update && apt install -y python3 python3-minimal python3-pip python3-dev python3-apt)
register: output
changed_when: output.stdout != ""
- setup: # aka gather facts
- name: Update System
apt:
upgrade: dist
update-cache: yes
tags:
- common
- basepackages-install
- name: Clean Apt Packages on System
apt:
autoremove: yes
autoclean: yes
tags:
- common
In diese Beispiel connected sich Ansible zu allen Hosts, führt keine Sammlung der System-Eigenschaften aus (gather_facts: no
) und installiert Python3 in einem sogenanten Pretask (vor allen anderen). Anschließend macht es ein Upgrade des Systems und entfernt automatisch alle Pakete, die nicht mehr gebraucht werden. Das kann man immer wieder ausführen und hätte ein Script zum regelmäßigen Update des Systems.
Idealerweise könnte man Prüfen, ob ein Kernelupdate dabei war und automatisch Neustarten:
- name: Check for kernel update
collect_kernel_info:
lookup_packages: false
register: kernel_update
- block:
- name: Reboot for kernel update
shell: "sleep 5 && shutdown -r now 'Kernel update detected by Ansible'"
async: 1
poll: 0
- name: Wait for server to come back online
wait_for_connection:
delay: 60
when: "kernel_update.new_kernel_exists"
- name: Collect kernel package information
collect_kernel_info:
register: kernel
- name: Remove old Debian/PVE kernels
apt:
name: "{{ ['linux-image-amd64'] + kernel.old_packages }}"
state: absent
purge: yes
Das fügt man ans Ende des Playbooks zusammen. Man sieht schnell, es gibt viele Wege. Relevant ist immer folgende Struktur.
Es gibt ein Playbook als Startpunkt. Dieses enthält Tasks und/oder verweißt auf Rollen. Diese Tasks und oder Rollen werden auf Gruppen von Hosts angewandt. Rollen können wieder verwendet werden. Tasks in Playbooks sind eher spezifisch für ein Playbook. Rollen nutzt man dann gerne, wenn man mehrere Playbooks hat und die in Teilen immer das gleiche ausführen sollen (commons halt). Diese Rollen sind immer lokal auf die eigene Umgebung zu betrachten.
Es gibt darüber hinaus noch Ansible Galaxy. Hier werden Rollen abgelegt von Entwicklern, die denken, dass die gebaute Rolle auch für andere interessant sein kann. Und es lohnt sich hier ein Blick. Entweder um Teile zu nutzen, zu lernen oder um sich die Arbeit zu erleichtern.
Und darüber hinaus gibt es in Ansible selber viele hundert Scripte, die zu Befehlen zusammengeführt werden, um komplexe Aufgaben nicht immer nur über command:
oder shell:
auszuführen (denk an die Idempotenz, die Mehrfachausführung). In dem Moment hilft ein Blick in die Liste der Befehle von Ansible selbst. Von Dateien teilweise ersetzen über Docker Verwaltung, Kernel und Packagemanager bis hin zu VM Verwaltung und Partitionierung ist wirklich alles irgendwo in irgendeinem Ansible Befehl zu finden.
Spätestens wenn man mit Ansible Scripten in virtuellen Umgebungen anfängt, neue Instanzen von Hosts oder VMs anzulegen, braucht man ein dynamisches Inventory. Ich habe mir eine Datenbank gebaut, die alle Informationen über mein “Inventar” enthält. Lege ich einen neuen Server an, versuche ich mir vor dem ersten Start genug Informationen zusammenzusammeln, um die Maschine mit Ansible zu erreichen. Bei Cloud-Diensten bekommt man die IP und Zugangsdaten per API. Bei VMs berechnet man die nächste freie IP aus einem Pool etc.
Die Daten in der Datenbank werden von einem Script zusammen gesammelt und in JSON Form ausgegeben, so wie Ansible es braucht. Das Inventory ist passwortgeschützt hinter einer URL zu erreichen. Für die CLI Umgebung von Ansible genügt dann ein Script (Bash oder Python), der die Daten der URL lädt und ausgibt. Damit man es in Ansible AWX benutzen kann, braucht man ein paar Sachen mehr.
In AWX unter Administration legt man einen neuen Credential Type an. Das ist zwar auch nur Benutzername+Passwort, aber der Script erwartet diese Zugangsdaten später in Umgebungsvariablen. Wir geben dem Type einen Namen und müssen die beiden YAML/JSON Felder mit Werten füllen.
Hier wird die Maske definiert für die Eingabefelder, die im AWX angezeigt werden, welchen Typ sie haben und wie sie heißen. Im Beispiel sind es Username+Password. In einem richtig guten Szenario könnte man noch die URL hinzufügen. Da die bei mir statisch im Script steht, kann man das erweitern - muss man aber nicht.
fields:
- id: username
type: string
label: Username
- id: password
type: string
label: Password
secret: true
required:
- username
- password
Die Injektor Konfiguration ist dann die Ausgabeseite. Wir brauchen Umgebungsvariablen, deswegen geben wir sie dort mit einem konkreten Namen aus.
env:
INVENTORY_CREDENTIALS: '{{ username }}:{{ password }}'
INVENTORY_PASSWORD: '{{ password }}'
INVENTORY_USERNAME: '{{ username }}'
Dann legt man unter Resources/Credentials einen neuen Credentials Eintrag an.
Und legt den folgenden Script unter Resources/Inventory Scripts ab. Diesen kann man dann später unter Resources/Inventories bei einem Inventar als Quelle angeben - zusammen mit den oben angegebenen Credentials.
#!/usr/bin/env python
import os
import sys
import argparse
import json
import requests
class AcobyInventory(object):
def __init__(self):
self.inventory = {}
self.read_cli_args()
# Called with `--list`.
if self.args.list:
self.inventory = self.acoby_inventory()
# Called with `--host [hostname]`.
elif self.args.host:
# Not implemented, since we return _meta info `--list`.
self.inventory = self.host_inventory()
# If no groups or vars are present, return an empty inventory.
else:
self.inventory = self.empty_inventory()
print(json.dumps(self.inventory, indent=2, sort_keys=True))
def acoby_inventory(self):
try:
url = 'https://server/v1/inventory'
headers = {"Accept": 'application/json'}
if "INVENTORY_USERNAME" in os.environ:
username = os.environ.get('INVENTORY_USERNAME')
else:
return self.empty_inventory()
if "INVENTORY_PASSWORD" in os.environ:
password = os.environ.get('INVENTORY_PASSWORD')
else:
return self.empty_inventory()
result = requests.get(url, auth=requests.auth.HTTPBasicAuth(username,password))
return json.loads(result.text)
except:
return {}
# Host inventory for testing.
def host_inventory(self, hostname):
return {
'_meta': {
'hostvars': {}
}
}
# Empty inventory for testing.
def empty_inventory(self):
return {
'_meta': {
'hostvars': {}
}
}
# Read the command line args passed to the script.
def read_cli_args(self):
parser = argparse.ArgumentParser()
parser.add_argument('--list', action = 'store_true')
parser.add_argument('--host', action = 'store')
self.args = parser.parse_args()
# Get the inventory.
AcobyInventory()
Installation auf MacOS
curl https://bootstrap.pypa.io/get-pip.py | sudo python3
sudo pip3 install ansible netaddr passlib
ansible-galaxy install -r roles/requirements.yml --force
Beim Einfügen eines neuen Hosts muss der Bootstrapprozess dort ausgeführt werden.
ansible-playbook -i inventory bootstrap.yml -e 'ansible_port=22' --limit <host>
Dadurch wird die Maschine für Ansible vorbereitet und die common Role installiert.
Im folgenden kann man dann auch wieder das gesamte Playbook ausführen. Da wir darin Passwörter ablegen, brauchen wir ein Vault-Kennwort. Dieses liegt im Keystore und nicht im Repository.
ansible-playbook -i inventory --vault-password-file vault.pass site.yml
Man kann sich ein paar gesammelte Details mit dem Setup Modul anschauen
ansible -i inventory -m setup <host>
Ansonsten hier mal eine Auswahl von Playbooks.
ansible-playbook -i inventory --vault-password-file vault.pass vms.yml
ansible-playbook -i inventory --vault-password-file vault.pass hosts.yml
ansible-playbook -i inventory --vault-password-file vault.pass reverseproxies.yml
Hinzufügen einer neuen VM geht z.B. mit
ansible-playbook -i inventory --vault-password-file vault.pass create_proxmox_kvm.yml -e "pve_node=proxmox pve_guest=vm pve_guest_ipv4=ipv4 pve_guest_ipv6=ipv6" --diff
Hier finden sich einige nützliche und interessante Links an, die sich im Laufe der Recherche angesammelt haben.
Make your own dynamic inventory list https://docs.ansible.com/ansible/latest/dev_guide/developing_inventory.html#developing-inventory
Custom Inventory https://www.jeffgeerling.com/blog/creating-custom-dynamic-inventories-ansible
Custom Inventory Scripts https://docs.ansible.com/ansible-tower/latest/html/administration/custom_inventory_script.html
https://github.com/bertvv/ansible-role-hosts/blob/master/templates/etc_hosts.j2
https://docs.ansible.com/ansible-tower/latest/html/administration/custom_inventory_script.html
Wir müssen im Inventory-File Passwörter hinterlegen und die sollen verschlüsselt sein. Diese kann Ansible transparent entschlüsseln. Das Erzeugen eines Schlüssels erfolgt mit:
ansible-vault encrypt_string --vault-password-file vault.pass 'password' --name 'the.secret'
Das Ergebnis wird auf dem Bildschirm geschrieben und kann so in die Passwortdatei überführt werden. Da das ein ziemlich langer Text ist, kann man den wie folgt im JSON Format kürzen:
"the.password": {
"__ansible_vault": "$ANSIBLE_VAULT;1.1;AES256\n36663736376262....3730\n"
},
Linksammlung:
Als am Anfang der IPv4 Welt der 4 Milliarden Addressen umfassende Zahlenraum definiert wurde, war nicht klar, welche Auswirkungen diese Entscheidung hatte.
Das heute fast jeder Mensch ein Handy hat, welches eine Adresse braucht, um sich im Internet bewegen zu können, war da noch nicht abzusehen. Man hat sich lange damit beholfen, via NAT separate Adressräume zu bilden. Aber mittlerweile hat man als Admin beim Aufbau von VPNs so häufig mit Adressraumkollisionen zu tun, dass man schon über weitere Maßnahmen wie VLAN nachdenken muss. Das es speziell im ostasiatischen Raum langsam eng wird, führte zu erhöhtem Druck, sich diesem Thema anzunehmen.
Deshalb hat man sich IPv6 ausgedacht. Der Adressraum wirkt im ersten Moment riesig. Eine Adresse stammt aus dem Bereich 2^128. Das ist eine enorm große Menge.
Leider verschwendet man diese Adressen. Bei einigen Hostern kriegt man pro virtueller Maschine einen 64er Block. Das sind 17 Trillionen Adressen. Wofür man die in einer einzelnen VM gebrauchen könnte, kann ich beileibe nicht sagen. Mir fallen keine Szenarien ein. Selbst wenn man den IPv4 Adressraum mit seinen 4 Milliarden Adressen darin spiegeln will, kann man das auch wieder 4 Milliarden mal tun. Wenn man zB. also in der VM mit Docker Prozessvirtualisierung betreibt, könnte man Milliarden von Adressen benutzen - ich denke, die CPU und der RAM wird früher aufgeben.
Nun, mal abgesehen von den Absurditäten mit den riesigen Zahlen muss man sich auch einige Themen genauer anschauen. IPv6 wirkt zunächst ziemlich gleich zu IPv4. Mal abgesehen davon, dass die Adressen anders geschrieben werden, gibt es eine große Gemeinheit. Man hat sich entschieden kein NAT mehr zuzulassen. Da kann man verschiedener Meinung sein, aber spätestens wenn es um Datenschutz oder Absicherung von inneren und externen Systemen geht, kommt man da schnell an Punkte, an denen man bisher einfach anders gedacht hat. Eine zentrale Instanz für das Routing (NAT) braucht es eigentlich nicht mehr und so mit gerät auch das Thema Firewall, was häufig mit Routing kombiniert wird, schnell in Bedrängnis.
Hier finden sich einige nützliche und interessante Links an, die sich im Laufe der Recherche angesammelt haben.
Docker hat das mit der IPv6 Unterstützung ziemlich versaut, weil man dort einfach sagt, installier es, konfigurier es und gut ist - leider wird bei IPv6 aber auch das Portforwarding ignoriert und jeder Port der in einem Container offen ist, wird nach außen offen gemacht, egal ob gewollt oder nicht. Und das ist natürlich Mist, daher die Krücke mit einem Image, dass beim Docker-Daemon prüft, wenn neue Services hochkommen und dann die Routen und Firewallregeln auch auf IPv6 anwendet.
Interessant ist dabei evtl auch, dass man das nutzen kann im Zusammenhang mit Acoby, dass dort gesagt wird, von welchem Loadbalancer aus die Ports offen sind (statt 0.0.0.0)
GlusterFS ist ein einfaches und nützliches verteiltes Dateisystem.
Hat man Dateien, die auf mehreren Servern synchron gehalten werden sollen, ist Gluster ideal. Man kann mit Gluster eine Reihe von unterschiedlichen Redundanz-Szenarien abbilden. Zum Beispiel kann man RAID-ähnliche Spiegelungen (0,1,5,10) aufbauen, in dem man bei der Anlage der Volumes die Zahl der Kopien definiert. Auf diese Weise lassen sich die Platten mehrerer Server verknüpfen und grosse Datenmengen ablegen.
Ein Wort der Warnung vorweg. Egal wie man es anstellt, die synchrone Verteilung von Daten verbraucht immer auch Netzwerkresourcen und ist von dessen Kapazität direkt abhängig. selbst für wenige Datenänderungen (ein schreibender Nutzer) kann ein Server mit einem 1Gbs Interface zu wenig sein. Die Server müssen darüber nicht nur den Dienst anbieten, sondern auch den Datenabgleich zwischen allen Knoten. Ausserdem ist Gluster für Dateien da, das Verteilen von Datenbankdateien oder Sockets ist kritisch und kann bei starker Netzauslastung für Fehler sorgen. Wir konnten das bei besagtem Setup nach nur wenigen Tagen Betrieb reproduzieren. Gluster muss nicht, aber sollte eine separate Netzwerkkarte für sich alleine haben und die sollte man gut im Auge behalten.
Was in dem Setup aber gut geht, sind reine Filesysteme mit grossen Readanteil, wie zB HTDoc-Verzeichnisse. Da Gluster auf Pfaden aufsetzt und damit keine eigenen Partitionen braucht, ist das Anlegen relativ einfach auf auf Maschinen mit nur einer Platte (VM) möglich.
Gluster ist als Paket bei Debian dabei und daher ist eine Installation denkbar einfach.
apt-get install glusterfs-server
service glusterd start
Damit ist der Server installiert und der Service bereit zur Nutzung. Zu beachten ist natürlich, dass das auf allen Nodes zu erfolgen hat.
Damit die Nodes sich gegenseitig kennt, beginnt man auf einem der Nodes ein “Probe” auf die anderen.
server1:$/> gluster peer probe server2.fqdn
Dadurch wird nicht nur eine Art Ping ausgeführt. Die Server tauschen auch im Zweifel Schlüssel aus und kennen sich von nun an. Man kann auch einen Server wieder aus dem Cluster werfen.
Die Konfiguration geschieht in zwei Phasen. Zunächst brauchen wir Volumes, die sich alle Nodes teilen. Die Teilung erfolgt auf drei Methoden - alles wird gespiegelt auf allen gleich, ein Teil wird gespiegelt, so dass alle Dateien mindestens x mal vorhanden sind oder es wird gestretcht - also aller Speicher wird genutzt, die Dateien liegen aber nur einmal vor und werden nur von der einen Quelle gelesen. Man kann das sehr gut mit den RAID Leveln vergleichen, wobei letzterer die geringste Ausfallsicherheit hat, erster maximal sicher ist.
Nun kommt es darauf an, wie man die Volumes konfiguriert und Gluster führt hier einen Begriff ein, der Brick heißt. Ich stelle mir vor, dass Brick ein Teil des Gluster-Clusters ist. Alle Steine zusammen ergeben das Volume. Was ich nicht verstehe, warum man bei Gluster derart unterscheidet zwischen dem Node und dem Brick und die Beispiele es einem Anfänger hier auch unnötig kompliziert machen. Letzlich ist es nur ein Pfad.
Man gibt also für ein Volume pro Node an, wo die Dateien für das Volume abgelegt sind. Ich tue das pro Volume und pro Node immer am gleichen Ort pro Server. Also legen wir auf allen Server ein Verzeichnispfad an.
mkdir -p /srv/gfs/gv0
In diesem Verzeichnis liegen zukünftig alle Daten unseres Volumes gv0. Dann können wir das Volume anlegen.
gluster volume create gv0 replica 2 server1.fqdn:/srv/gfs/gv0 server2.fqdn:/srv/gfs/gv0 force
In dem Verzeichnis wird ein .glusterfs Verzeichnis angelegt, in dem Gluster alle seine Verwaltungsdaten ablegt. Der Rest wird verteilt und kann so auch benutzt werden. Ich empfehle aber eher einen Mountpoint anzulegen und das GlusterFS zu mounten und richtig daraufzu schreiben.
Dafür braucht es einen Client. Auch den gibt es natürlich bei Debian als Paket.
apt-get install glusterfs-client
mount -t glusterfs ac-sh0003.acoby.de:gv2 /mnt/
Will man darüber hinaus Gluster für die Volumes von Docker verwenden, empfehle ich folgendes:
docker plugin install --alias gfs trajano/glusterfs-volume-plugin --grant-all-permissions --disable
docker plugin set gfs SERVERS=server1.fqdn,server2.fqdn
docker plugin enable gfs
Dann kann man sehr simple Docker anweisen, die Volumes mit dem PlugIn GFS verteilt abzulegen.
volumes:
htdocs:
driver: gfs:latest
name: "gv0/htdocs"
Will man die Performance optimieren, gibt es eine Vielzahl an Parametern, die man setzen kann.
gluster volume set gv0 nfs.disable on
gluster volume set gv0 performance.cache-max-file-size 128MB
gluster volume set gv0 performance.cache-size 256MB
gluster volume set gv0 performance.flush-behind on
Und hiermit kann man den Zustand des Systems einsehen.
gluster volume info
gluster volume status
gluster volume heal gv0 info summary
Hier finden sich einige nützliche und interessante Links an, die sich im Laufe der Recherche angesammelt haben.