# Mini-omschrijving XS4ALL voice platform * een aantal hardware LXD hosts, met tig geheugen en tig CPU * verspreid over meerdere lokaties * alle applicaties draaien in LXD containers op deze hosts * naasts deze hosts zijn er nog een paar SBC clusters, die zorgen voor goede en veilige interconnectie met externe partijen (onze uplink naar het KPN netwerk en alle connecties met de klantapparatuur / softphones) Configuration management is een hybride setup: * puppet beheert een stuk van de LXD host configuratie * Ansible beheert andere stukken van de LXD host configuratie, plus alle LXD containers in volledigheid (dus geen 2 kapiteins op 1 schip, maar een strakke verdeling in taken) De op te lossen vraagstukken voor Ansible waren vooral: * Hoe kunnen we geautomatiseerd LXD containers optrekken? * Hoe beheren we het OS op al die LXD containers vanuit 1 Ansible omgeving? * Hoe kunnen we development en acceptatie handig inrichten? Verschillende soorten inzet van Ansible: * Nieuwe LXD containers optrekken indien nodig (one-shot runs) * Configuratiemanagement LXD containers en LXD hosts (one-shot runs) * Onderhoudstaken op regelmatige basis (periodieke runs in cron) # Waarom LXD? * Het platform bestaat uit heel veel verschillende applicaties * Allemaal in het verleden gebouwd als Ubuntu hosts (hardware en VM's) * Te veel niet gebruikte resources, containerization was een logische keuze * Veel van de applicaties hebben ook een veelheid aan scheduled jobs en support scripts * Statisch netwerk, omdat SBC's geen dynamische config kennen * Daardoor ook geen hippe autoscaling behoefte * Migratie naar LXD binnen XS4ALL was strak tijdgebonden (gekoppeld aan einde Edutel) Doordat de beschikbare tijd gelimiteerd was en LXD de mogelijkheid bood om de bestaande omgeving vrijwel 1-op-1 na te bouwen op basis van LXD systeemcontainers, was de keuze voor virtualisatie snel gemaakt. Het platform omzetten naar bijvoorbeeld Docker was ongetwijfeld mogelijk geweest, maar dat zou heel veel extra werk hebben opgeleverd en voor extra complexiteit op de netwerklaag hebben gezorgd. Jammer? Nee! Met LXD en Ansible hebben we een mooi evenwicht bereikt tussen de bekende cattle en pets analogie. Het voice platform heeft diverse pets in zich, maar die kunnen zonder blikken of blozen naar de slachtbank. # Inhoud van de demo * Twee VM's in KVM, die dienst doen als LXD host * Netwerktopologie * LXD clustering * Inrichten van een Ansible container * # Installatie VM's voor de demo * 2 VM's: sidn-demo-01, sidn-demo-02 * virtual machine met NAT netwerk: 192.168.56.0/24, gateway/DNS 192.168.56.1 * Ubuntu 18.04 LTS #### Upgrade na installatie ```text sidn-demo-0X# apt update && apt upgrade -y ``` #### LXD snap-versie installeren ipv apt-versie Standaard gebruikt 18.04 een apt-package voor LXD. Deze is prima bruikbaar, maar voor een meer up-to-date feature set is het beter om de snap installatie te gebruiken. Snap is op dit moment de aanbevolen method voor het draaien van LXD. ``` sidn-demo-0X# snap refresh sidn-demo-0X# snap install lxd sidn-demo-0X# lxd.migrate ("no" voor automatisch verwijderen LXD) sidn-demo-0X# apt remove --purge lxd lxd-client (want zo zie ik wel hoe ver deze is) ``` #### LXD initialiseren Voor deze demo: ```text sidn-demo-0X# lxd init > clustering: no > storage pool: yes, default, dir > connect to MAAS: no > network bridge: no > available over the network: yes, all interfaces, port 8443, password ``` Op productie doen we het (uiteraard) anders: * ZFS als filesystem * "Available over network" alleen op het management VLAN # Netwerktoplogie Belangrijke keuze qua netwerk: elk netwerk interface in de LXD host is ondergebracht in een bridge interface. * Elke container kan simpel op elk benodigd netwerk "ingeplugd" worden door een interface van de container aan de bridge toe te voegen. * Elke container is daarmee ook direct exposed op dat netwerk. * Containers kunnen ook containers op een andere LXD host bereiken, mits het layer 2 netwerk doorgetrokken is uiteraard. * Containers kunnen eenvoudig naar een andere LXD host worden verplaatst, mits de nieuwe host dezelfde netwerkbridges heeft geconfigureerd. __Praktisch gezien lijkt dit op hoe je het netwerk met hardware hosts en switches zou regelen__ #### Netwerk bridges op de LXD hosts Tijdens lxd init maken we niet de standaard lxdbr0 bridge aan. Die bridge is alleen voor intern gebruik binnen de host. Wanneer je services beschikbaar wilt stellen aan andere systemen, dan moeten daar trucs voor uitgehaald worden (vgl. met Docker/Traefik). In plaats daarvan worden de bridges door onszelf geconfigureerd in Netplan. Netplan-config op productie ziet er ongeveer zo uit: ```yaml network: version: 2 ethernets: eno1: {} # management network (untagged) eno2: {} # SIP network (SIP signalling & services, tagged) eno3: {} # public interface (untagged) vlans: # SIP core: proxy & SBC signalling vlan.6 id: 6 link: eno2 # SIP applications (voicemail, mediaserver, class5 services) vlan.9 id: 9 link: eno2 bridges: br-mgmt: dhcp4: no dhcp6: no interfaces: ["eno1"] addresses: ["172.17.4.10/24"] br-public: dhcp4: no dhcp6: no interfaces: ["eno3"] gateway4: 194.109.16.129 addresses: ["194.109.16.132/26"] nameservers: addresses: ["194.109.6.66", "194.109.9.99"] br-sipcore: dhcp4: no dhcp6: no interfaces: ["vlan.6"] addresses: ["10.160.64.132/24"] br-sipapp: dhcp4: no dhcp6: no interfaces: ["vlan.9"] addresses: ["10.116.1.132/24"] ``` Met een dergelijke setup is het heel eenvoudig om de LXD containers beschikbaar te maken binnen bepaalde netwerksegmenten. De containers kunnen een of meer netwerk interfaces krijgen, die verbonden kunnen worden met de benodigde netwerken. Dat zien er dan bijvoorbeeld zo uit (afwijkende bridge-namen ivm de ruimte): ```text GATEWAY | | +-------------------------+------+----------public--+ | | | | +-------------------------+----------SIP------------+ | | | | | | | | +-------------------------+---mgmt------------------+ | | | | | | | | | | | | | | | | | | +---O------O------O----+ +---O------O------O----+ +---O------O------O----+ | eno3 eno2 eno1 | | eno3 eno2 eno1 | | eno3 eno2 eno1 | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | br3 br2 br1 | | br3 br2 br1 | | br3 br2 br1 | | | | \ | | | | | | | | | | | | | | | | | \ | | | | | | | CONTAINER1 | | | | `--CONTAINER3 | | | CONTAINER4 | | | | | | | | | | | CONTAINER2 | | | | CONTAINER5 | | | | | | | +----------------------+ +----------------------+ +----------------------+ LXD Host 1 LXD Host 2 LXD Host ... n ``` Hier is bijvoorbeeld CONTAINER1 aangesloten op het public en het SIP netwerk, terwijl CONTAINER2 aangesloten is op SIP en mgmt. CONTAINER4 is alleen aangesloten op het SIP netwerk, en kan via dat netwerk op layer 2 CONTAINER1 en CONTAINER2 ook bereiken. __Voor de demo gebruiken we een simpeler opzet:__ ```yaml network: version: 2 ethernets: enp1s0: {} bridges: br-demo: dhcp4: no dhcp6: no interfaces: ["enp1s0"] addresses: ["192.168.56.150/24"] # of 151 voor sidn-demo-02 gateway4: "192.168.56.1" nameservers: addresses: ["192.168.56.1"] ``` Dus maar 1 bridge "br-demo" en het "fysieke" interface enp1s0 van de VM zit "ingeplugd" in deze bridge. In een plaatje: ```text KVM host 192.168.56.1 (gateway + DHCP + DNS) | | +------192.168.56.0/24----+ | | +------O---------------+ +------O---------------+ | enp1s0 | | enp1s0 | | | | | | | | +---O------------+ | | +---O------------+ | | | br-demo | | | | br-demo | | | | 192.168.56.150 | | | | 192.168.56.151 | | | +--O---O---------+ | | +--O--O----------+ | | | | | | | | | | | CONTAINER2 | | | CONTAINER4 | | | | | | | | CONTAINER1 | | CONTAINER3 | | | | | +----------------------+ +----------------------+ KVM guest: sidn-demo-01 KVM guest: sidn-demo-02 ``` #### Netwerk interfaces op de LXD containers Met bovenstaande setup is het dus mogelijk om een LXD container netwerk interfaces te geven die "ingeplugd" worden in een of meer van de br-\* bridges. Een SIPproxy server bijvoorbeeld, krijgt drie netwerk interfaces in drie verschillende bridges: * br-mgmt: voor host management, o.a. Ansible aansturing * br-public: op een SIPproxy voor SSH toegang en OS updates * br-sipcore: voor SIP signalling verkeer richting SBC's Om binnen een container het herkennen van de netwerk interfaces zo simpel mogelijk te houden, wordt de naamgeving van de bridges gevolg. Bijvoorbeeld een interface dat in bridge "br-aap" wordt geplugd, heet dan "if-aap". Veel handiger dan "eth0" o.i.d. Op de SIPproxy zul je dan ook de interfaces if-mgmt, if-public en if-sipcore terug kunnen vinden. __Interfaces vanuit LXD profielen__ Om bij het optrekken van een LXD container de benodigde netwerk interfaces aan te maken, wordt gebruik gemaakt van profielen. Er zijn verschillende profielen gemaakt voor verschillende netwerkbehoeftes: ```text # lxc profile list +---------------+---------+ | NAME | USED BY | +---------------+---------+ | ansible-acc | 1 | +---------------+---------+ | default | 11 | +---------------+---------+ | sipproxy | 2 | +---------------+---------+ | voicemail | 2 | +---------------+---------+ | voiceservices | 2 | +---------------+---------+ ``` Hier het stuk van het profiel wat voor het netwerk van de hierboven beschreven SIPproxy container wordt gebruikt: ```text productie# lxc profile show sipproxy config: ---8<------- devices: if-mgmt: name: if-mgmt nictype: bridged parent: br-mgmt type: nic if-public: name: if-public nictype: bridged parent: br-public type: nic if-sipcore: name: if-sipcore nictype: bridged parent: br-sipcore type: nic ---8<------- ``` Noot: Voor het voice platform is dit een handige setup. Uiteraard kan elke mogelijk setup worden gemaakt op basis van de behoefte. Als de behoefte alleen maar een lokaal werkende bridge met private IP space was, dan zou ik het geheel nog steeds wel handmatig inrichten zoals hierboven, alleen dan met een loopback bridge met een duidelijk naam als "br-local" of "br-private". #### Aanmaken LXD profile voor de demo Op basis van de gevolgde lxd init methode, is er al een default profiel aangemaakt voor LXD. Deze is heel erg basaal, omdat we geen netwerk bridge hebben geconfigureerd: ```text sidn-demo-0X# lxc profile show default config: {} description: Default LXD profile devices: root: path: / pool: default type: disk name: default used_by: [] ``` Het beheer van profielen regelen we primair vanuit Ansible, maar op dit punt van de demo maak ik met de hand een profiel aan. ```text sidn-demo-0X# lxc profile create demo sidn-demo-0X# lxc profile edit demo config: user.user-data: | # cloud-config package_upgrade: true packages: - python3 timezone: Europe/Amsterdam description: SIDN demo profile devices: if-demo: name: if-demo nictype: bridged parent: br-demo type: nic root: path: / pool: default type: disk sidn-demo-0X# lxc profile show demo ``` In deze config zijn wat cloud-init statements toegevoegd om te laten zien dat cloud-init te gebruiken is in een profiel. De installatie van Python is handig, omdat daarmee de container klaar is voor Ansible. Echter, in het voice platform wordt dit niet zo gedaan. Daar worden wat bootstrapping Ansible recipes gebruikt om Python op een vers systeem te krijgen. Voordeel is dat de Ansible-methode ook voor oudere versies van Ubuntu en voor niet cloud-init images te gebruiken is. Noot: In de praktijk hebben wij tot op heden alleen maar timezone en netwerkconfiguratie gepusht met de cloud-init configuratie. En zelfs de netwerkconfguratie slechts deels, omdat deze met de cloud-init van Ubuntu 14.04 nog niet mogelijk was. # LXD clustering Of beter gezegd: __geen__ LXD clustering. Een cluster zorgt er voornamelijk voor dat je een aantal LXD hosts aan elkaar koppelt, die onderling gaan uitwisselen welke containers er op de hosts draaien. Daarna krijg je met bijvoorbeeld "lxc list" niet alleen inzicht in de containers die op de lokale host draaien, maar ook in alle containers die op alle cluster hosts draaien. Het klinkt heel goed: LXD clustering. Functioneel heb ik er echter nog weinig heil in gevonden voor onze setup. Bovendien heb ik op clustersystemen regelmatig problemen gezien met het goed werkend houden van het cluster, door bijvoorbeeld het uit sync raken van de quorum database (gebaseerd op dqlite, wat een distributed sqlite database is.) Voordat clustering bestond in LXD liepen wel al wel tegen een probleem aan dat clustering probeert op te lossen: het werken met containers, verspreid over meerdere LXD hosts. Onze aanpak: * De LXD daemon laten luisteren op het netwerk (zoals bij lxd init al aangezet) * Je kunt vanuit de ene LXD host een andere LXD host als "remote" toevoegen onder een bepaalde naam. * Vanaf dan kunnen lxc commando's de naam van de remote gebruiken om operaties op de andere host te doen. * We hebben alle LXD hosts op deze manier kruislings gekoppeld, zodat vanuit elke host elke andere host aangesproken kan worden. Om dit op te zetten, zorg dan eerst dat alle LXD hosts remote access actief hebben, mocht dat nog niet geconfigureerd zijn vanuit lxd init. Doe het volgende op elke host: ``` sidn-demo-0X# lxc config set core.https_address [::]:8443 sidn-demo-0X# lxc config set core.trust_password SuperGeheimGoedWachtwoordHorseStaple ``` Nu kunnen de remotes op elke LXD host worden aangemaakt. Merk op dat we ook voor de lokale LXD daemon een remote aanmaken. Dat maakt een aantal zaken vanuit Ansible een stuk simpeler, omdat we dan vanaf elke willekeurige host bijv `lxc list sidn-demo-01:` kunnen doen en niet op die bestreffende host `lxc list local:` hoeven te gebruiken Als wachtwoord gebruiken we uiteraard het bovenstaande wachtwoord. ``` sidn-demo-0X# lxc remote add --accept-certificate sidn-demo-01 192.168.56.150 sidn-demo-0X# lxc remote add --accept-certificate sidn-demo-02 192.168.56.151 ``` Vanaf nu kun je vanaf elk van de twee LXD hosts commando's uitvoeren op alle LXD hosts, bijv: ``` sidn-demo-0X# lxc list -cns sidn-demo-01: sidn-demo-0X# lxc list -cns sidn-demo-02: sidn-demo-0X# lxc launch ubuntu:18.04 sidn-demo-02:test ``` __Ja, maar, hoe wordt dit gebruikt dan?__ Het belangrijkste gebruik hiervan, is dat er vanuit cron op elke LXD host een job draait die een `lxc list` uitvoert op alle remotes. Daarmee wordt een lijst gemaakt waarin de containers en hun LXD hosts staan. Vanuit die lijst wordt een bestand gemaakt, met daarin een serie bash aliases voor `lxc exec : bash`. Als ik wil inloggen op bijvoorbeeld de container `tel-prd-sipproxy-02`, dan kan ik na het sourcen van het gegenereerde aliassen bestand simpelweg `tel-prd-sipproxy-02` als commando uitvoeren op een willekeurige LXD host, waarna ik binnenval op een shell op die container. # De Ansible container De Ansible management host is een container op het voice platform zelf. Vanwege de kip/ei problematiek en het feit dat deze container niet steeds opnieuw gebouwd gaat worden, bootstrappen we deze container met de hand. De handigste manier voor de netwerkconfiguratie van deze container is gebruik maken van een user.network-config. sidn-demo-01# vi /tmp/config version: 1 config: - type: physical name: if-demo subnets: - type: static address: 192.168.56.160 netmask: 255.255.255.0 gateway: 192.168.56.1 dns_nameservers: [192.168.56.1] # zodat dezelfde resolving als de KVM host wordt gebruikt Bouw de container op en start hem: sidn-demo-01# H=ansible sidn-demo-01# lxc init --storage default --no-profiles ubuntu:18.04 $H sidn-demo-01# lxc config device add $H if-demo nic name=if-demo nictype=bridged parent=br-demo sidn-demo-01# lxc config set $H volatile.if-demo.hwaddr 00:16:3e:00:00:a0 # mainly for VirtualBox sidn-demo-01# lxc config set $H user.network-config - < /tmp/config sidn-demo-01# lxc config show $H sidn-demo-01# lxc start $H sidn-demo-01# lxc exec $H bash ansible# apt update && apt upgrade -y && apt autoclean -y && apt autoremove -y Noot: Voor deze setup zou ook het eerder aangemaakte "demo" profiel gebruikt kunnen worden. Echter, op het voice platform doen we het op deze manier, omdat we de profielen vanuit Ansible opzetten en het benodigde profiel daarom niet noodzakelijkerwijs al bestaat op de LXD host. Op de container is nu te zien dat het layer 2 netwerk bereikbaar is vanuit de container: ansible# ip a (laat zien dat er nu een if-demo interface bestaat) ansible# ping 192.168.56.150 (een ping naar de eigen LXD host) ansible# ping 192.168.56.151 (een ping naar de andere LXD host) Connectie Ansible -> managed hosts ================================== Even een uitstapje naar de manier waarop we vanuit Ansible verbinding gaan maken naar de LXD hosts en de LXD containers, voor het uitvoeren van de nodige commando's. In Ansible zit support voor het gebruik van lxc in plaats van ssh voor het verbinden met de te configureren hosts. Aanvankelijk zijn we daar 100% voor gegaan, maar helaas bleek dat dit een behoorlijke impact op de snelheid heeft. Met een SSH koppeling, kan de SSH verbinding open worden gehouden en kunnen commando's in rap tempo's naar een host worden gestuurd. De lxc connector _doet dit helemaal niet_. Voor elk commando wordt een nieuwe connectie opgezet. Om de zaken te versnellen, gebruiken we nu een hybride setup: - Management van de LXD hosts wordt volledig met SSH gedaan vanuit de Ansible container. - Het bootstrappen van nieuwe containers wordt gedaan door met SSH naar een LXD host te gaan (delegate), en vervolgens worden van daaruit (met de hierboven geconfigureerde remotes) met lxc connecties de containers opgetrokken en geconfigureerd tot het niveau dat de Ansible container er met SSH bij kan. - Het verder configureren van containers wordt volledig met SSH gedaan, direct vanuit de Ansible container. Ansible SSH toegang Ansible -> LXD hosts ============================================ Om de LXD hosts te kunnen beheren met SSH, is het nodig dat er een SSH key wordt gegenereerd en dat deze naar de hosts wordt gekopieerd. ansible# ssh-keygen -b 2048 -N '' -f /root/.ssh/id_rsa sidn-demo-0X# mkdir -p /root/.ssh sidn-demo-0X# chmod 700 /root/.ssh sidn-demo-0X# touch /root/.ssh/authorized_keys sidn-demo-0X# chmod 600 /root/.ssh/authorized_keys sidn-demo-0X# lxc exec sidn-demo-01:ansible cat /root/.ssh/id_rsa.pub >> /root/.ssh/authorized_keys Hierna moet het volgende werken: ansible# ssh 192.168.56.150 touch .hushlogin ansible# ssh 192.168.56.151 touch .hushlogin Installatie van de Ansible software =================================== De basis infrastructuur staat. Nu kan de benodigde software op de Ansible host worden geïnstalleerd. Op het voiceplatform wordt overal gebruik gemaakt van Python3 voor Ansible. ansible# apt install -y python3 python3-pip ansible# pip3 install ansible The Ansible configuration is managed in a git repository. Go get! ansible# apt install -y git ansible# REPO=https://git.makaay.nl/mauricem/sidn-lxd-ansible-demo.git ansible# git clone $REPO /etc/ansible