Kubernetes edge loadbalancing met Haproxy

Geplaatst door

In Kubernetes weet je niet waar je applicatie draait . Die informatie is uiteraard wel op te vragen maar eigenlijk boeit het gewoon niet. De orchestrator zorgt er voor dat de workload draait op een server met voldoende resources. De onderliggende infra wordt vanuit applicatieperspectief simpelweg minder boeiend zolang het maar werkt. Hoewel dit een mooi concept is, introduceer je hiermee ook nieuwe uitdagingen. Aan de buitenkant is vaak 1 ipadres actief terwijl je aan de binnenkant niet weet op welke server je applicatie actief is. In dit artikel vertel ik over een manier hoe je on premise met opensource oplossingen dit probleem kan oplossen.

Bestaande commerciële oplossingen

Er zijn verschillende bestaande kant en klare oplossingen die je kan gebruiken. Zo heb je de F5 met hun BIG-IP oplossing en heeft ook AVI networks een cloud native Loadbalancer. Beide maken het mogelijk om met hun appliance de taken van de Ingress controller over te nemen.

Globaal gesproken werken de verschillende appliances het zelfde: Een controller-pod leest de Kubernetes-API uit en configureert via de externe loadbalancer via hun eigen API. De appliances worden dan bijvoorbeeld onderdeel van het POD netwerk of krijgen een BGP peer en kunnen op die manier direct communiceren met Kubernetes services of pods.

Hoewel het goed werkt, hebben de commerciële appliances duidelijk ook een nadeel. Ze zijn vaak niet bepaald goedkoop. Als je budget iets krapper is, zal je mogelijk blij zijn met een andere oplossing.

Bestaande opensource oplossingen

Naast de commerciële oplossingen zijn er ook opensource alternatieven. Haproxy biedt namelijk een oplossing. De werking is dan iets anders dan bij de eerst genoemde commercieel appliances: Je kan haproxy in TCP mode draaien en laten loadbalancen over alle Kubernetes nodes op de NodePorts waarop de gebruikte ingress controller actief is voor HTTP en HTTPS verbindingen. Hoewel de NodePort een primitieve oplossing is (De Kubernetes service wordt dan exposed op alle cluster nodes op de zelfde poort), werkt het best goed in situaties waarbij het aantal cluster nodes niet continu op/af schaalt. De Haproxy configuratie is statisch en kan zonder extra software niet direct met Pod/Service IP’s van pods communiceren waardoor de laag 4 TCP configuratie met de NodePort van de ingress service overblijft, of software voor VXLAN/BGP toegevoegd moet worden.

Haproxy met de Dataplaneapi

Toen ik in mei 2018 op Kubecon Europe in Kopenhagen was, spraken we daar mensen van Haproxy Technologies (het bedrijf achter Haproxy). Ik kreeg het idee dat ze met hun statische configuratie van de opensource versie terrein verloren. Er waren alleen third party initiatieven voor een Ingress controller. Ook de configuratie van Haproxy zelf was zonder extra third party software een statische bedoeling. Ze vertelden toen dat er hard gewerkt werd aan verbetering. Ze hebben duidelijk niet stil gezeten! Er is inmiddels een eigen Ingress controller en ook is er een nieuwe REST API ontwikkeld waarmee Haproxy grotendeels geconfigureerd kan worden.

“Connecting the dots”

Haproxy heeft een API. Kubernetes heeft een API. Laten we beide werelden samenvoegen! Recentelijk schreef ik een script dat de Kubernetes API controleert op nodes met de status “Ready|NotReady” en de role “master|none”. Vervolgens wordt de configuratie van Haproxy via de API bijgewerkt zodat de backend-definities daar een actuele lijst van servers hebben. Het resultaat is dat de externe loadbalancer automatisch met je cluster op/af schaalt.

Dit artikel is te kort om het hele verhaal van A tot Z uit te werken maar aan de hand van fragmenten en voorbeelden hoop ik je inzicht te geven in de verschillende aspecten en manieren van werken.

Het kubernetes deel
Het begint met het uitlezen van de Kubernetes API. Je wil immers op de hoogte blijven van node gerelateerde wijzigingen. Voor de veiligheid gebruik ik hier een apart service account die slechts die rechten heeft die nodig zijn. Meer rechten zouden de boel alleen maar onveiliger maken. Laad de volgende YAML definities in Kubernetes:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: haproxycontrollerpod
  namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: haproxycontrollerpod
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: haproxycontrollerpod
subjects:
- kind: ServiceAccount
  name: haproxycontrollerpod
  namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: denit
rules:
 
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get", "list", "watch"]

Haal het gemaakte token uit het kubernetes secret en zet het in file “/home/username/.kube/tokenfile”:

kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep haproxycontrollerpod | awk '{print $1}')

Kopieer ca.crt vanaf 1 vanaf de master server en zet het in “/home/username/.kube/ca.crt”:

cp /etc/kubernetes/pki/ca.crt 

Genereer vervolgens met deze files een kubeconfig (let op dat je je huidige kubeconfig niet overschrijft!):

KUBECONFIG="/home/username/.kube/config"
KUBEMASTERIP="ip van kubernetes API"
KUBECA="/home/.username/ca.crt"
TOKEN=$(cat /home/username/.kube/tokenfile"
>$KUBECONFIG
kubectl config --kubeconfig $KUBECONFIG set-credentials haproxycontrollerpod --token=${TOKEN}
kubectl config --kubeconfig=$KUBECONFIG set-cluster kubernetes --server=https://$KUBEMASTERIP:6443 --certificate-authority $KUBECA --embed-certs=true
kubectl config --kubeconfig=$KUBECONFIG set-context kubernetes --cluster=kubernetes --user=haproxycontrollerpod
kubectl --kubeconfig=$KUBECONFIG config use-context kubernetes

Nu ben je in staat om te testen of de beperkte rechten je ook laten zien wat je wil. De gemaakte Kubeconfig kan je beter niet in je scripts gebruiken. Je kan echter wel eenvoudig een commando uitvoeren daaraan een “verbosity level” koppelen. Hiermee zie je zowel de headers als de JSON data die de APi in de body als repsonse terug geeft:

kubectl get nodes -o wide -v9

En zie hieronder een fragment van de output. Gezien de hoeveelheid JSON data in de body, heb ik die achterwege gelaten:

root@hostname:~/khcp# kubectl get nodes -o wide -v9
I0815 09:43:59.207349   17651 loader.go:359] Config loaded from file:  /root/.kube/config
I0815 09:43:59.216447   17651 round_trippers.go:419] curl -k -v -XGET  -H "Accept: application/json;as=Table;v=v1beta1;g=meta.k8s.io, application/json" -H "User-Agent: kubectl/v1.15.1 (linux/amd64) kubernetes/4485c6f" 'https://10.0.65.10:6443/api/v1/nodes?limit=500'
I0815 09:43:59.227593   17651 round_trippers.go:438] GET https://10.0.65.10:6443/api/v1/nodes?limit=500 200 OK in 11 milliseconds
I0815 09:43:59.227634   17651 round_trippers.go:444] Response Headers:
I0815 09:43:59.227643   17651 round_trippers.go:447]     Content-Type: application/json
I0815 09:43:59.227650   17651 round_trippers.go:447]     Date: Thu, 15 Aug 2019 07:43:59 GMT
I0815 09:43:59.227750   17651 request.go:947] Response Body:

Een dergelijk verzoek is eenvoudig te verwerken in een Python script:

#!/usr/bin/python3
import requests

K8sMasterIp = "KUBERNETESMASTERIP"
K8sMasterPort = "6443"
K8stoken="KUBERNETESSERVICEACCOUNTTOKEN"

#FUNCTION GET KUBERNETES NODE status
def GetKubernetesNodeStatus():
  hed = {'Authorization': 'Bearer ' + K8stoken}
  urlparams = {'limit': 500}
  response = requests.get('https://'+K8sMasterIp+':'+K8sMasterPort+'/api/v1/nodes', params=urlparams, headers=hed, verify='ca.crt')
  jsondata = response.json()
  print(jsondata)
  #items = (jsondata['items'])
  ##print(K8sNodes)
  for status in jsondata['items']:
    print(status['status'])
GetKubernetesNodeStatus()

De JSON data moet vervolgens gefilterd worden. Je wil alleen de Status, de Role en het IP van de kubernetes nodes weten zodat je de gefilterde output in een loop kan vergelijken met het vorige request zodat in het geval van een wijziging, de Haproxy API benaderd kan worden om daar de configuratie te wijzigen.

Het Haproxy deel

Haproxy krijgt een paar specifieke regels in de configuratie die pas vanaf versie 2.0 ondersteund zijn.

global
  master-worker

program api
    command /usr/local/bin/dataplaneapi --host 0.0.0.0 --port 8001 -b /usr/sbin/haproxy -c /etc/haproxy/haproxy.cfg  -d 5 -r "systemctl reload haproxy" -u dataplaneapi -t /tmp/haproxy1

userlist dataplaneapi
        user admin password $5$OURnYrKms2K/Vu$b0L/CGeJQdLOnqMjBtmZmQjgoKAaZBwfBLOCwfKoa3A

Je ziet dat je in de haproxy configuratie de dataplaneapi kan aanroepen. Deze is helaas nog niet uit een apt repository te installeren. Binary releases zijn gelukkig wel te vinden op: https://github.com/haproxytech/dataplaneapi/releases . Ik verwacht dat het een kwestie van tijd is voordat hier een package van beschikbaar komt.

Hoewel elk request op de API een opzichzelfstaand request is, kan je ze samenvoegen door te werken met transacties. Op die manier kan je meerdere aanpassingen in losse API calls stoppen en de wijzigingen effectief maken met slechts 1 reload van Haproxy.

Om zo’n transactie aan te maken, moet je eerst weten welk versienummer de haproxy config heeft. Na elke succesvol afgesloten transactie, wordt dit nummer opgehoogd. Tussen het openen en sluiten van de transactie kun je zoveel losse requests toevoegen als je wil. zie hieronder een voorbeeld:

#!/usr/bin/python3
import requests

ApiScheme = "http"
ApiUser = "admin"
ApiPass = "password"
ApiHostname = "haproxyApiIPaddress"
ApiPort = "8001"

#FUNCTION: Get Haproxy config version
def GetVersion():
  global HaproxyConfigVersion
  HaproxyConfigVersion = ""
  response = requests.get(ApiScheme+'://'+ApiHostname+':'+ApiPort+'/v1/services/haproxy/configuration/global', auth=(ApiUser, ApiPass))
  jsondata = response.json()
  HaproxyConfigVersion = (jsondata['_version'])

#FUNCTION: CREATE HAPROXY TRANSACTION ID
def GenerateTransactionId():
  global HaproxyTransactionId
  HaproxyTransactionId = ""
  urlparams = {'version': HaproxyConfigVersion}
  response = requests.post(ApiScheme+'://'+ApiHostname+':'+ApiPort+'/v1/services/haproxy/transactions', params=urlparams, auth=(ApiUser, ApiPass))
  jsondata = response.json()
  HaproxyTransactionId = (jsondata['id'])

#FUNCTION: APPLY and FINISH TRANSACTION ID
def ApplyTransaction():
  response = requests.put(ApiScheme+'://'+ApiHostname+':'+ApiPort+'/v1/services/haproxy/transactions/'+HaproxyTransactionId, auth=(ApiUser, ApiPass))
  jsondata = response.json()
  status = (jsondata['status'])

GetVersion()
GenerateTransactionId()
ApplyTransaction()

Je kan er voor kiezen om in de haproxy configuratie alvast frontends en backends (zonder servers) aan te maken. Op die manier hoef je met deze constructie enkel node informatie verkregen uit de Kubernetes API te verwerken in requests om servers in bestaande backends te verwijderen of juist toe te voegen.

Tot slot
Geheel in Kubernetes stijl is het aan te raden om een pod te maken waarin dit script actief is. Zorg er voor dat wachtwoorden in een Secret staan en configuraties in een ConfigMap. In productieomgevingen zal je het gedrag van je script goed moeten testen. Hoe gedraagt het script zich als er een API endpoint niet bereikbaar is of de reponses een onverwachte inhoud hebben? Tevens is Haproxy in deze constructie nog niet redundant uitgevoerd. Door op deze manier met de materie bezig te zijn, is dat echter geen onmogelijke uitdaging! Kijk ook op de API references van Kubernetes en Haproxy:

Kubernetes API reference : https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.15/
Haproxy API reference: https://www.haproxy.com/documentation/dataplaneapi/latest/

Loadbalancing op basis van TCP en NodePort voelt wat primitief aan in een wereld waarin BGP, en integratie met overlay/container netwerken bij appliances makkelijk in te stellen zijn. Echter werkt het voor clusters van enigszins beperkte grootte of projecten met een beperkter budget erg goed.

Kubernetes is een opensource project maar de meeste ontwikkelingen worden gedaan door de grote cloudproviders. Die zorgen er voor dat het vooral goed kan integreren met hun platform. Dit soort vraagstukken zijn al lang opgelost bij de grote public clouds. De API’s voor Haproxy en Kubernetes maken het mogelijk om on premise mooie dingen te bouwen waar nog geen kant en klare oplossing voor is. De tijd dat de beheerder niets van ontwikkeling hoefde te weten ligt inmiddels ver achter ons.

Geef een reactie