Was ist LXC?
LXC ist eine Benutzeroberfläche für die Linux-Kernel-Containment-Funktionen. Mit einer leistungsstarken API und einfachen Tools können Linux-Benutzer System- oder Anwendungscontainer problemlos erstellen und verwalten werden.
Funktionen
LXC verwendet Kernelfunktionen, um Prozesse zu enthalten:
- Kernel-Namespaces (mount, pid, uts, ipc, network und user)
- Apparmor- und SELinux-Profile
- Seccomp-Richtlinien
- Chroots
- Kernel-Fähigkeiten
- Gruppen
LXC-Container werden oft als etwas in der Mitte zwischen einer chroot- und einer vollwertigen virtuellen Maschine betrachtet. Das Ziel von LXC ist es, eine Umgebung zu schaffen, die einer Standard-Linux-Installation so nahe wie möglich kommt, ohne dass ein separater Kernel erforderlich ist.
Komponenten
LXC besteht aus einigen separaten Komponenten:
- Die liblxc-Bibliothek
- Mehrere Sprachbindungen für die API:
- python3
- lua
- Go
- Ruby
- und weitere.....
- Eine Reihe von Standardwerkzeugen zur Steuerung der Container
- Vorlagen für die Container
Linux-Container bestehen aus drei Teilen:
- KomponentenEine Konfigurationsdatei mit Informationen über die verwendeten Ressourcen
- Eine Datei im fstab-Format, welche die Einhängepunkte für den Container enthält. Diese Informationen kann man alternativ auch direkt in der Konfigurationsdatei angeben.
- Das Root-Dateisystem des Containers.
Soll nur eine einzelne Anwendung in einem Container ausgeführt werden, spricht man von einem Anwendungscontainer. Bei Anwendungscontainern sollte man sich genau überlegen, welche Ressourcen man isolieren will. Man kann zum Beispiel den Rechnernamen (Hostname) ändern und das Netzwerk isolieren. Um Konflikte zwischen Dateien zu vermeiden, kann man Teile des Dateisystem neu einhängen, oder man erstellt ein eigenes Root-Dateisystem und kann Teile des Haupt-Dateisystems über Bind-Mounts gemeinsam nutzen. Wenn keine Konfiguration angegeben wird, werden nur Prozessnummern, System V IPC und Einhängepunkte virtualisiert und isoliert.
Wird ein vollständiges System in einem Container eingerichtet, spricht man von einem Systemcontainer. Die Konfiguration eines Systemcontainers ist einfacher, da man sich keine Gedanken über die isolierten Ressourcen machen muss, weil alles isoliert werden muss.
Folgende Einstellungen sind in der Konfigurationsdatei möglich:
lxc.utsname | Hostname für den Container |
lxc.tty | Anzahl der ttys innerhalb des Containers |
lxc.pts | Pseudo ttys für den Container anlegen. |
lxc.mount | Kann eine fstab-Datei übergeben werden. |
lxc.mount.entry | Einhängepunk in Format einer fstab-Zeile. |
lxc.rootfs | Root-Dateisystem des Containers |
lxc.network.type | Netzwerkvirtualisierung, folgende Werte sind möglich: empty, veth, vlan, macvlan und phys |
lxc.network.flags | Aktiviert das Netzwerkinterface: up |
lxc.network.link | Das vom Container verwendete Netzwerkinterface. |
lxc.aa_profile |
Container können mit AppArmor abgesichert werden, AppArmor hat ein Standardprofil. Es kann jedoch auch ein eigenes Profile angegeben werden oder mit "unconfined" komplett abgeschaltet werden. |
Container Konfiguration
Es gibt zwei Möglichkeiten Container zu Konfigurieren.
- Konfigurations-Template (Einträge werden in CONTAINERNAME/config übernommen)
- Schreibt jede Konfig für einen Container /var/lib/lxc/CONTAINERNAME/config
Es macht jedoch mehr Sinn ein Template zu schreiben und nur ergänzende Einstellungen am Container durchzuführen. Debian erstellt solch ein Template mit nur einem Eintrag, damit lassen sich zwar Container aufbauen und starten, aber ohne Netzwerk, was wenig Sinn macht. Dieses Template ist unter /etc/lxc/default.conf
lxc.network.type = empty
Mit den oben beschriebene Werte kann das Template Konfiguriert werden.
Beispiel Konfiguration:
lxc.network.type = veth lxc.network.link = lxcbr0 lxc.network.falgs = up lxc.network.hwaddr = 00:16:3e:xx:xx:xx lxc.start.auto = 1 lxc.group = onboot
Erklärung:
Die Zeile lxc.network.type = veth ein virtuelles Ethernet-Pair wird erstellt, wobei eine Seite dem Container zugewiesen wird und die andere Seite an einer Bridge gebunden ist. Über lxc.network.link = lxcbr0 wird LXC angewiesen die veth Interface an die Brücke lxcbr0 anzubinden und das Netzwerk wird mit lxc.network.flags = up gestartet. Die MAC-Adressen für Virtualisierungen sollten aus dem Bereich 00:16:3e stammen, was über die Zeile lxc.network.hwaddr = 00:16:3e:xx:xx:xx realisiert wird. LXC ergänzt die MAC-Adresse Automatisch. Die MAC-Adresse Identifiziert ein Netzwerkgerät, warum die MAC-Adresse aus dem Bereich 00:16:3e sein soll habe ich bisher noch keine weiteren Informationen im Netz gefunden, viele Links zu diesem Thema existieren leider nicht mehr. Ich gehe davon aus das im Code von LXC dieser MAC-Bereich besser verarbeitet wird.
LXC-Konfiguration
Sucht man in den Hilfeseiten (Manpages) danach wie LXC Konfiguriert wird, wird man nur schwer fündig. Deswegen habe ich mir den Bootprozess von LXC einmal näher angesehen.
[Unit] Description=LXC network bridge setup After=network-online.target Before=lxc.service [Service] Type=oneshot RemainAfterExit=yes ExecStart=/usr/lib/x86_64-linux-gnu/lxc/lxc-net start ExecStop=/usr/lib/x86_64-linux-gnu/lxc/lxc-net stop [Install] WantedBy=multi-user.target
Jetzt ist klar, LXC wird von "/usr/lib/x86_64-linux-gnu/lxc/lxc-net" gestartet. Schauen wir uns das noch etwas genauer an.
file /usr/lib/x86_64-linux-gnu/lxc/lxc-net
/usr/lib/x86_64-linux-gnu/lxc/lxc-net: POSIX shell script, ASCII text executable
Super, es handelt sich also um ein Bash-Script. Schaut man sich dieses an, verraten einem die ersten 24 Zeilen einiges.
#!/bin/sh - distrosysconfdir="/etc/default" varrun="/run/lxc" varlib="/var/lib" # These can be overridden in /etc/default/lxc # or in /etc/default/lxc-net USE_LXC_BRIDGE="true" LXC_BRIDGE="lxcbr0" LXC_BRIDGE_MAC="00:16:3e:00:00:00" LXC_ADDR="10.0.3.1" LXC_NETMASK="255.255.255.0" LXC_NETWORK="10.0.3.0/24" LXC_DHCP_RANGE="10.0.3.2,10.0.3.254" LXC_DHCP_MAX="253" LXC_DHCP_CONFILE="" LXC_DOMAIN="" LXC_IPV6_ADDR="" LXC_IPV6_MASK="" LXC_IPV6_NETWORK="" LXC_IPV6_NAT="false"
In Zeile 7 und 8 steht der Hinweis, dass die Default Werte für lxc in "/etc/default/lxc-net" Konfiguriert werden können, diese wird von "/etc/default/lxc" eingelesen und verarbeitet. Glücklicherweise haben die Entwickler aussagekräftige Variablennamen verwendet, so das die Zeilen 10 bis 24 relativ einfach zu verstehen sind.
Nehmen wir einfach mal an, du hast nichts gegen das 10er Netz und du möchtest lediglich die Standard Bridge verwenden und deine LXC-Container mit einer Statischen IP-Adresse betreiben (was zu empfehlen ist) und ein Domain-Name (diese hat nichts mit der Domain des Servers zu tun, sondern ist lediglich nur für interne zwecke gedacht) betreiben, dann könnte deine /etc/default/lxc-net wie folgt aussehen.
USE_LXC_BRIDGE="true"
LXC_DHCP_CONFILE="/etc/lxc/dhcp.conf"
LXC_DOMAIN="knasan.local"
In der Datei /etc/lxc/dhcp.conf werden die Container für Dnsmasq eingetragen, somit laufen die LXC mit einer statischen IP-Adresse obwohl der Container lediglich auf DHCP gestellt bleibt. In dieser Datei müssen die Einträge wie folgt eingetragen werden.
dhcp-host=containername,IP-Adresse
Hier ein Beispiel:
dhcp-host=c1,10.0.3.10 dhcp-host=c2,10.0.3.20 dhcp-host=c3,10.0.3.30 dhcp-host=c4,10.0.3.40 dhcp-host=c5,10.0.3.50
Nachdem der Dienst lxc-net neugestartet wurde, können die Container eingerichtet werden und erhalten sofort die passende IP-Adresse.
Container erstellen
Ein Container wird mit dem Kommando "lxc-create" erstellt. Die wichtigsten Parameter dieses Kommandos:
-n | Name des Containers |
-t | Template, unter /usr/share/lxc/templates gibt es einige |
-f | Alternative Konfigurationsdatei |
Wenn du ein Debian Container mit dem Namen c1 erstellen möchtest, musst du einfach nur
lxc-create -n c1 -t debian
ausführen. Nach einer Kurzen Zeit findest du im Ordner /var/lib/lxc den Container c1.
Anwendungscontainer Container starten und beenden
Eine Anwendung in einem Container startet man mit:
lxc-execute -n CONTAINERNAME Befehl
Systemcontainer starten
Einen Systemcontainer startet man mit:
lxc-start -n CONTAINERNAME -d
Man erhält eine Konsole, auf der man sich anmelden kann. Diese Konsole hat eine feste Größe, unabhängig von der tatsächlichen Größe des Terminals und ist nur bedingt geeignet. Die mit lxc-console geöffneten Konsolen haben diese Einschränkung nicht.
lxc-console -n CONTAINERNAME
Login in Container
Im Container selbst kann man sich einen SSH-Damon installieren, was sehr praktikabel ist. Für schnelle Aktinen, Konfiguration oder falls ssh nicht funktioniert kann man auch "lxc-attach" verwenden. Jedoch ist in dieser Umgebung nicht alles möglich, da nicht alle Pseudo-Treiber für die Konsole geladen werden. Aber die meisten Befehle lassen sich daran ausführen (wenn auch nur bedingt).
lxc-attach -n CONTAINERNAME
Weitere Tools:
Es gibt einige tools für lxc. Schau dir einfach mal die Manpage von lxc an, dort wirst du fündig. Ganz Interessant finde ich lxc-top um zu sehen wie sich deine Container so auf dem Server machen und wie viel Performance diese verschlingen. Hier mal eine Liste von Nützlichen lxc-tools.
lxc-attach | Aufführen von Programme innerhalb eines Containers. Gibt man nichts an, wird die Bash gestartet. |
lxc-checkconfig | Prüft den aktuellen Kernel auf lxc Support. |
lxc-console | Startet eine Konsole für den angegebenen Container |
lxc-copy | Kopiert eine vorhandenen Container |
lxc-create | Erstellt einen Container |
lxc-destroy | Löscht einen vorhandenen Container |
lxc-execute | Führt eine Application aus (Anwendungscontainer) |
lxc-freeze | Alle Prozesse des Containers einfrieren |
lxc-info | Informationen über einen Container abfragen |
lxc-ls | Liste der auf dem System vorhandenen Container |
lxc-start | Startet einen Container |
lxc-stop | Schaltet einen Container ab |
lxc-snapshot | Erstellt ein Snapshot für einen existierenden Container |
Kernel-Namespaces
Linux-Prozesse bilden eine einzige Hierarchie, bei der alle Prozesse verwurzelt sind init. Normalerweise können privilegierte Prozesse in diesem Baum andere Prozesse verfolgen oder beenden. Mit dem Linux-Namespace können wir viele Hierarchien von Prozessen mit ihren eigenen „Unterstrukturen“ haben, sodass Prozesse in einem Teilbaum nicht auf Prozesse in einem anderen Baum zugreifen können.
Ein Namespace Umschließt eine globale Ressource so, dass Prozesse in diesem Namespace über eine eigene isolierte Instanz dieser Ressource verfügen. Nehmen wir als Beispiel den PID-Namespace. Ohne Namensraum gehen alle Prozesse hierarchisch von PID 1 (init) ab. Wenn wir einen PID-Namespace erstellen und darin einen Prozess ausführen, wird dieser erste Prozess zur PID 1 in diesem Namespace. In diesem Fall haben wir eine globale Systemressource (Prozess-IDs) verpackt. Der Prozess, der Namespace erstellt, bleibt zwar im übergeordneten Namespace, macht jedoch das untergeordnete Element zum Stamm der neuen Prozessstruktur.
Dies bedeutet jedoch nur, dass die Prozesse im neuen Namespace den übergeordneten Prozess nicht sehen können, der übergeordnete Prozess-Namespace jedoch den untergeordneten Namespace. Und die Prozesse innerhalb des neuen Namensraums verfügen jetzt über zwei PIDs: eine für den neuen und eine für den globalen Namensraum.
Der Linux-Kernel verfolgt jetzt die PIDs von Prozessen mithilfe der upid-Struktur anstelle eines einzelnen pid-Werts. Die upid-Struktur gibt Auskunft über die PID und die Namespaces, in denen die PID gültig ist.
Namespace Typen
- cgroup: Dies isoliert das Cgroup-Stammverzeichnis ( CLONE_NEWCGROUP ).
- IPC: Isoliert System V IPC und POSIX-Nachrichtenwarteschlangen ( CLONE_NEWIPC )
- Netzwerk: isoliert Netzwerkgeräte, Ports usw. ( CLONE_NEWNET )
- Mount: isoliert Mountpunkte ( CLONE_NEWNS )
- PID: isolierte Prozess-IDs ( CLONE_NEWPID )
- Benutzer: isoliert Benutzer- und Gruppen-IDs ( CLONE_NEWUSER )
- UTS: isoliert den Hostnamen und den NIS-Domänennamen ( CLONE_NEWUTS )
Als Teil der Namensraumverwaltung bietet Linux folgende APIs:
- clone() - plain old clone () erstellt einen neuen Prozess. Wenn wir ein oder mehrere CLONE_NEW -Flags an clone () übergeben, werden für jedes
Flagneue Namespaces erstellt, und der untergeordnete Prozess wird zu einem Mitglied dieser Namespaces. - setns() - Ermöglicht einem Prozess, einem vorhandenen Namespace beizutreten. Der Namensraum wird durch eine Dateideskriptorreferenz auf eine der proc/[pid]/ns Dateien angegeben.
- unshare() - verschiebt den Aufrufenden Prozess in einen neuen Namespace, der mit den Argumenten CLONE_NEW erstellt wurde. Es können mehrere solcher Flags angegeben werden.
Hinweis: Für den PID-Namespace MUSS clone() aufgerufen werden, da dies nur während der Erstellung eines neuen Prozesses erstellt werden kann. Der durch clone() erzeugte Prozess hat die PID 1. Für den PID-Namespace ist unshare() nicht nützlich.
Netzwerk-Namensraum
Nehmen wir an, wir haben einen neuen PID-Namespace. Der in diesem neuen Namensraum sitzende Prozess wartet auf Port 80 auf eingehende Anforderungen. Dies bedeutet, dass alle anderen Prozesse im gesamten System daran gehindert werden. Dies ist keine sehr hilfreiche Isolation. Hier kommen Netzwerk-Namespaces.
Netzwerk-Namespaces helfen internen Prozessen, unterschiedliche Netzwerkschnittstellen einschließlich der lo (localhost) Schnittstellen zu sehen! Aber das ist nur die halbe Wahrheit. Wenn wir neue Netzwerk-Namespaces haben, müssen wir virtuelle Netzwerkschnittstellen einrichten, die viele Namespaces zusammen mit einem Routing-Prozess umfassen, der im globalen Namespace ausgeführt wird, um den Datenverkehr zu verarbeiten und an den korrekten Namespace weiterzuleiten.
Wie bereits erwähnt, isolieren andere Namespaces eine bestimmte globale Ressource und beschränken den Zugriff des inneren Prozesses auf die eigene Sandbox.
Noch keine Kommentare