Another CYSO DevOps related article, this time about implementing ‘missing’ features on a VMware Private Cloud. This post has also been posted to the CYSO blog.

De gebruikelijke plek om Kubernetes te installeren is op een Public Cloud. Alle aanbieders ondersteunen een manier om automatisch een cluster uit te rollen, met diepgaande integratie met de rest van de cloud. Denk hierbij aan Google GKE, Microsoft AKS en ook Fuga EMK.

Maar wat als je Kubernetes wilt draaien vanaf een Private Cloud, bijvoorbeeld VMware? Bepaalde features die je misschien gewend bent zijn dan vaak niet aanwezig, zoals Load Balancers met een automatisch publiek IP, en Storage Classes die automatisch een volume aanmaken.

Een volledig automatisch CI/CD proces is voor CYSO een vereiste. Omdat we bij CYSO alle onze diensten draaien op onze eigen VMware Private Cloud, moeten we deze twee blockers oplossen. Laten we kijken hoe we dit kunnen doen.

Loadbalancers met een Public IP

Op een Kubernetes platform met Cloud integratie zal je zien dat zodra er een Service van type LoadBalancer wordt aangemaakt, er direct een IP wordt gealloceerd:

kind: Service
apiVersion: v1
metadata:
  name: traefik-ingress
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app.kubernetes.io/name: traefik
    app.kubernetes.io/instance: service
  ports:
    - protocol: TCP
      port: 80
      name: http
    - protocol: TCP
      port: 443
      name: https

Het laden van deze definitie resulteert dan in het volgende:

NAME             TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                      AGE
...
traefik-ingress  LoadBalancer   10.104.146.73   93.94.224.164   80:32109/TCP,443:30450/TCP   47d

Op een 'normaal’ Kubernetes platform zal er echter niet automatisch een IP worden gealloceerd. Kubernetes weet immers niet uit zichzelf welke IP's er beschikbaar zijn. In een Public Cloud omgeving kan het platform zelf dat vertellen aan Kubernetes. Op een Private Cloud omgeving moet dat echter worden toegevoegd aan Kubernetes. Eén manier om dat te doen is met MetalLB.

MetalLB

MetalLB is een loadbalancer implementatie voor Kubernetes, die integreert met het lokale netwerk. Er zijn meerdere manieren waarop MetalLB zijn werk kan doen, maar de meest interessante is de BGP modus. Hiermee kan direct aan een router verteld worden welke IP's er in gebruik zijn op Kubernetes, zodat verkeer dynamisch gerouteerd wordt naar de juiste nodes. Het enige wat nog handmatig gedaan moet worden is het instellen van de BGP peer configuratie op de router, en het selecteren van een IP range die MetalLB mag uitdelen op Kubernetes.

In dit voorbeeld gaan we ervoor zorgen dat MetalLB de range 1.2.3.4 - 1.2.3.8 mag uitdelen. We doen de aanname dat de router(s) waarnaar geadverteerd wordt al ingesteld zijn en BGP advertenties accepteren van alle nodes in het Kubernetes cluster. Het is ook aan te raden om enkel advertenties te accepteren voor IP's uit de reeks die we geven aan MetalLB, dit wordt prefix matching genoemd.

De eerste stap is MetalLB zelf installeren met een manifest:

kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.1/manifests/metallb.yaml

Daarna moeten we MetalLB vertellen hoe hij moet peeren via BGP, en welke IP's hij mag uitdelen:

kubectl create -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
apiVersion: v1
data:
  config: |
    peers:
    - peer-address: 10.x.x.1
      peer-asn: 65559
      my-asn: 65559
      password: "foo"
    address-pools:
    - name: public-pool
      protocol: bgp
      addresses:
      - 1.2.3.4-1.2.3.8
EOF

Zodra MetalLB draait en de configuratie is geladen, zal aan de router kant te zien zijn dat elke node uit het Kubernetes cluster zich heeft aangemeld als peer. Op een FortiGate ziet dat er zo uit:

# get router info bgp summary 
BGP router identifier 172.x.x.1, local AS number 65559
BGP table version is 1
1 BGP AS-PATH entries
0 BGP community entries

Neighbor        V         AS MsgRcvd MsgSent   TblVer  InQ OutQ Up/Down  State/PfxRcd
10.x.x.101      4      65559    4767    5436        0    0    0 00:52:00        2
10.x.x.102      4      65559    4812    5475        0    0    0 01:36:10        2
10.x.x.103      4      65559    3190    3644        0    0    0 02:32:26        2

Total number of neighbors 3

De laatste stap is het aanmaken van een Service met type LoadBalancer. Hierbij zijn er twee opties: MetalLB het IP laten uitkiezen, of deze handmatig aangeven. MetalLB zal ervoor zorgen dat een bepaald IP niet tweemaal wordt gebruikt. In dit voorbeeld rollen we een simpele webapplicatie met een LoadBalancer uit:

kubectl create -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami
  labels:
    deployment: whoami
spec:
  replicas: 3
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
      - name: whoami
        image: containous/whoami
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: whoami-lb
  annotations:
    metallb.universe.tf/address-pool: public-pool
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app: whoami
  ports:
  - name: http
    port: 80
    targetPort: 80
EOF

Na het opstarten van de Pods zal MetalLB beginnen met het adverteren van een route. Een route wordt enkel geadverteerd naar nodes die de Pod draaien. Zodra een Pod wordt verplaatst of verwijderd dan zullen de routes automatisch worden aangepast. In Kubernetes en op een FortiGate is dit zichtbaar:

$ kubectl get svc
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
whoami-lb    LoadBalancer   10.111.65.124   1.2.3.4         80:31174/TCP   24h

# get router info routing-table bgp
B       1.2.3.4/32 [200/0] via 10.x.x.101, vlxxxx, 00:57:11
                   [200/0] via 10.x.x.102, vlxxxx, 00:57:11
                   [200/0] via 10.x.x.103, vlxxxx, 00:57:11

Het resultaat is vergelijkbaar met een LoadBalancer op een Public Cloud.

Storage Classes

Voor dynamische volumes op Kubernetes loop je op een Private Cloud tegen dezelfde problemen aan als met LoadBalancers: het platform is vaak niet in staat om flexibel volumes aan te maken en dit direct te gebruiken in Kubernetes. Zelfs als de storage laag wel dynamisch volumes kan aanmaken, dan moet deze storage alsnog worden geformateerd en gemount op de juiste locatie, afhankelijk van welke Pods het volume nodig hebben.

Kubernetes heeft ondersteuning voor een groot aantal Volume plugins, met elk hun eigen voor- en nadelen. Eén daarvan is de AccessMode, of: kan één of meerdere Pods de storage tegelijk benaderen. Bepaalde workloads hebben lees- en schrijfrechten nodig voor hetzelfde volume, denk bijvoorbeeld aan het CMS van een website die uploads moet verwerken. Elke Pod moet dan een geüpload bestand kunnen wegschrijven.

De Volume plugins die de ReadWriteMany modus ondersteunen en beschikbaar zijn op een Private Cloud zijn de volgende: CephFS, GlusterFS en NFS. Uit deze lijst heeft enkel GlusterFS ondersteuning voor Storage Classes. In dit voorbeeld zullen we GlusterFS op zo'n manier integreren dat Volumes automatisch kunnen worden aangemaakt vanuit Kubernetes, zonder dat daarvoor een handmatige actie nodig is op de storage laag.

Gluster + Heketi

Gluster is een schaalbare storage techniek die een bestandsysteem kan aanbieden via het eigen GlusterFS protocol of NFS. Voor het benaderen hiervan vanuit Kubernetes is echter een API nodig, omdat de storage laag vaak gescheiden staat van de Kubernetes nodes. Gluster heeft zelf geen ingebouwde API, maar via een apart project genaamd Heketi kan deze wel worden toegevoegd.

Het opzetten van Gluster samen met Heketi vereist wat werk en expertise, maar hoeft maar één keer te gebeuren. Voor dit artikel gaan we ervan uit dat er al een Gluster cluster met Heketi aanwezig is, en zullen we ons focussen op de configuratie van Kubernetes. Hiervoor is het belangrijk dat we tijdens de installatie van Kubernetes het admin wachtwoord en de FQDN van de Heketi API opschrijven, deze hebben we nodig tijdens het configureren van Kubernetes.

Het admin wachtwoord voegen we als eerste toe aan Kubernetes als Secret:

kubectl create -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: heketi-secret
  namespace: kube-system
type: "kubernetes.io/glusterfs"
stringData:
  key: ADMIN_PASSWORD
EOF

Daarna moeten we ervoor zorgen dat op alle Kubernetes nodes dezelfde versie van gluster-client geïnstalleerd staat. Dit is afhankelijk van de Linux distributie, maar is meestal zo simpel als een:

$ apt install glusterfs-client

De laatste stap is het aanmaken van een Storage Class waarin we Kubernetes vertellen waar de Heketi API leeft, welke API credentials gebruikt moeten worden en welke soort volumes we willen aanmaken in Gluster. In dit voorbeeld maken we een Storage Class die volumes aanmaakt waarbij alles twee keer wordt opgeslagen voor redundantie, en we refereren naar het Secret die we eerder hebben aangemaakt:

kubectl create -f - <<EOF
apiVersion: storage.k8s.io/v1beta1
kind: StorageClass
metadata:
  name: gluster-heketi-external
provisioner: kubernetes.io/glusterfs
parameters:
  resturl: "http://gfs1:8080"
  restuser: "admin"
  secretName: "heketi-secret"
  secretNamespace: "kube-system"
  volumetype: "replicate:2"
EOF

Zodra de StorageClass is aangemaakt, kan Kubernetes automatisch volumes maken op Gluster. Er zijn meerdere manieren om dit te doen, maar de meest directe manier is om een PersistentVolumeClaim aan te maken:

kubectl create -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
 name: website-uploads
 annotations:
   volume.beta.kubernetes.io/storage-class: gluster-heketi-external
spec:
 accessModes:
  - ReadWriteMany
 resources:
   requests:
     storage: 2Gi
EOF

In reactie hierop zal Kubernetes een PersistentVolume aanmaken die aan de claim voldoet, dus met de gekozen StorageClass en de opgegeven grootte:

$ kubectl get pvc website-uploads 
NAME              STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS              AGE
website-uploads   Pending                                      gluster-heketi-external   12s

$ kubectl get pvc website-uploads 
NAME              STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS              AGE
website-uploads   Bound    pvc-03c57d35-afc8-4b62-8495-7c9ae4557f17   2Gi        RWX            gluster-heketi-external   27s

$ kubectl get pv pvc-03c57d35-afc8-4b62-8495-7c9ae4557f17
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                       STORAGECLASS              REASON   AGE
pvc-03c57d35-afc8-4b62-8495-7c9ae4557f17   2Gi        RWX            Delete           Bound    namespace/website-uploads   gluster-heketi-external            27s

Deze PersistentVolume is ook terug te vinden via Heketi, als we weten wat het interne ID is:

$ kubectl get pv pvc-03c57d35-afc8-4b62-8495-7c9ae4557f17 -ojson | jq -r '.spec.glusterfs.path' | sed 's/vol_//'
edcb81b740c8a4b048243bd2803cf73d

$ heketi-cli volume info edcb81b740c8a4b048243bd2803cf73d
Name: vol_edcb81b740c8a4b048243bd2803cf73d
Size: 2
Volume Id: edcb81b740c8a4b048243bd2803cf73d
Cluster Id: c1584b184593707ee6fc1bd613d59e1a
Mount: 10.xx.xx.132:vol_edcb81b740c8a4b048243bd2803cf73d
Mount Options: backup-volfile-servers=10.xx.xx.131
Block: false
Free Size: 0
Reserved Size: 0
Block Hosting Restriction: (none)
Block Volumes: []
Durability Type: replicate
Distribute Count: 1
Replica Count: 2
Snapshot Factor: 1.00

We hebben nu een volume met redundantie aangemaakt op Gluster door via Kubernetes daarom te vragen. Dit is vergelijkbaar met de werking op Public Clouds.

Het resultaat

Architectuur met MetalLB en GlusterFS

In het grote geheel van een Kubernetes cluster hebben we nu twee componenten toegevoegd:

  • Een mechanisme wat aan LoadBalancers IPs kan toewijzen en deze adverteert via BGP ( MetalLB).
  • Een mechanisme wat met PersistentVolumeClaims en StorageClasses automatisch volumes kan aanmaken op shared storage ( Heketi / Gluster).

Deze twee componenten zorgen er samen voor dat we alle features hebben die we kunnen verwachten van Kubernetes op een Public Cloud dienst, maar dan op onze VMware Private Cloud.