Algunos clientes requieren al círculo de QSO una mayor flexibilidad y capacidad de personalización en la conectividad de red de los pods en sus clusters de Kubernetes. Al permitir la asignación de múltiples interfaces de red a un solo Pod, Multus CNI facilita la implementación de escenarios de red complejos en Kubernetes.

En este post haremos un repaso genérico de varios conceptos de redes en Kubernetes y utilizaremos Multus CNI y el NM State Operator para añadir redes adicionales en pods y VMs en OpenShift Virtualization.

¿Qué es el Kubernetes network model?

El modelo de red de Kubernetes es el conjunto de conceptos, reglas y abstracciones que definen cómo se comunican los contenedores, pods y nodos dentro de un clúster de Kubernetes.

El modelo intenta solucionar los siguientes desafíos de comunicación en K8s a través de los siguientes métodos:

El modelo de red de Kubernetes se implementa a través de los Container Runtimes en cada nodo usando Container Network Interface (CNI) Plugins.

Cluster Node
Container Network Interface (CNI) Plugins.

¿Qué es un Plugin CNI?

Similar a los CRI y CSI, CNI es un proyecto de la CNCF que consta de una especificación y bibliotecas para escribir Plugins para configurar interfaces de red en contenedores de Linux, junto con una serie de plugins compatibles.

Kubernetes CRI CSI CNI

CNI se ocupa únicamente de la conectividad de red de los contenedores y de la creación, modificación, lectura o eliminación de los recursos asignados cuando se crea o elimina el contenedor. Debido a este enfoque, CNI tiene una amplia gama de soporte y la especificación es fácil de implementar.

CNI network

Plugins CNI por defecto o primarios

El plugin CNI Primario o por defecto es el plugin principal que gestiona las redes en nuestros clusters de K8s.

Hay múltiples Plugins CNI con diferentes implementaciones, pero todos los plugins principales o primarios deben resolver la comunicación pod a pod cumpliendo los siguientes requisitos de Kubernetes:

Una forma de agruparlos es en base a la integración nativa con Clouds, obteniendo así 3 grupos:

A continuación podemos ver ejemplos de esta agrupación:

Agrupación flavor

Plugins adicionales

Una vez tenemos los principales requisitos cubiertos, observamos que no todos los plugins CNI cubren todas las necesidades y por ello tenemos una gran variedad de soluciones.

Cloud native network.

Se puede tener una combinación de diferentes CNI para diferentes funciones, como Calico para network policies y Flannel para la gestión de redes de los pods.

No se pueden tener dos CNI diferentes que realicen exactamente la misma funcionalidad en el mismo clúster simultáneamente.

Una funcionalidad muy interesante es, por ejemplo, que un pod pueda comunicarse directamente con una red externa. Aquí es donde Multus CNI entra en escena 😀.

Multus CNI

Con Multus CNI, es posible configurar redes adicionales junto con la red de pods predeterminada, añadiendo interfaces de red a los pods en otras redes distintas a la red principal de pods.

Multus kubernetes.

El plugin Multus CNI actúa como un meta plugin al llamar a otros plugins CNI para funcionalidades de red avanzadas.

Pod con multus

Main Plugins

En OpenShift se proporcionan los siguientes plugins CNI para que Multus CNI los utilice cuando necesitemos añadir más redes a pods o máquinas virtuales en OpenShift Virtualization:

Bridge CNI

NetworkAttachmentDefinitions

El plugin de Multus CNI se configura a través de NetworkAttachmentDefinition (net-attach-def o NAD) CRD. Se puede configurar en:

Interfaz de red

Operadores relacionados

En la demo de este post utilizaremos además de Multus CNI los siguientes Operadores.

NMState Operator

El NMState Operator gestiona configuración de red de nodos declarativamente a través de la API de Kubernetes vía CRD.

Los 3 principales son:

Configuración de red de nodos

El kubernetes-nmstate-operator se ha incluido como componente en OCP Virtualization hasta la versión 4.10 de OCP. A partir de esta versión hay que instalar el Operador por separado.

Componente kubernetes-nmstate-operator

Openshift Virtualization Operator

En otros posts del blog de Paradigma ya os hemos comentado sobre OpenShift Virtualization y la necesidad de escenarios intermedios de migración a cloud native en la que convivan cargas de trabajo virtualizadas y en contenedores. Este Operador permite alojar y gestionar workloads virtualizados en la misma plataforma que los workloads basados en contenedores.

OpenShift Virtualization

La tecnología detrás de OpenShift Virtualization se desarrolla en la comunidad OpenSource de KubeVirt.

OpenShift Virtualization

Relación Multus CNI, NMState y OCP Virtualization

Llegados a este punto tendrás varias preguntas como por qué te he contado todo esto 😛.

Todo está relacionado: Con el NMState Operator podemos gestionar declarativamente interfaces de red físicas en los nodos y crear recursos (bridges) para que a través de Multus CNI añadamos redes adicionales a pods o VM de OpenShift Virtualization.

NMState.Operator

Casos de uso

Otra pregunta que te habrá surgido es por qué los clientes pueden querer todo este lío de CRDs.

Hemos visto, entre otros, los siguientes casos de uso:

Demo

En esta demo:

Nos quedaría algo así:

Single Node Cluster de OpenShift

NNS

Vamos a ver el estado actual de la configuración de red del nodo y vemos que no hay ninguna referencia al bridge br1 que queremos crear. En el siguiente Yaml vemos la información relevante del NNS (hay información omitida sobre routes, dns, etc.):

apiVersion: nmstate.io/v1beta1
kind: NodeNetworkState
metadata:
  name: ip-10-0-179-230.eu-west-1.compute.internal
status:
  currentState:
    interfaces:
    - ipv4:
        address:
        - ip: 10.0.179.230
          prefix-length: 17
        auto-dns: true
        auto-gateway: true
        auto-route-table-id: 0
        auto-routes: true
        dhcp: true
        enabled: true
        lldp:
        enabled: false
      mac-address: 0A:FA:D4:3D:B0:A7
      mtu: 9001
      name: ens6
      state: up
      type: ethernet
    - ipv4:
        address: []
        enabled: false
      ipv6:
        address: []
        enabled: false
      lldp:
        enabled: false
      mac-address: 0A:DB:EC:A1:B5:DF
      mtu: 1500
      name: eth1
      state: down
      type: ethernet
    - ipv4:
        address:
        - ip: 192.168.0.60
          prefix-length: 24
        auto-dns: true
        auto-gateway: true
        auto-route-table-id: 0
        auto-routes: true
        dhcp: true
        enabled: true
       lldp:
        enabled: false
      mac-address: 0A:AE:69:A1:16:FF
      mtu: 9001
      name: eth2
      state: up
      type: ethernet
    - ipv4:
        address:
        - ip: 127.0.0.1
          prefix-length: 8
        enabled: true
      ipv6:
        address:
        - ip: ::1
          prefix-length: 128
        enabled: true
      mac-address: "00:00:00:00:00:00"
      mtu: 65536
      name: lo
      state: up
      type: unknown

NNCP

Ahora vamos a utilizar una NodeNetworkConfigurationPolicy para levantar un bridge llamado br1 sobre la eth2.

# br1-eth2-policy.yaml
apiVersion: nmstate.io/v1
kind: NodeNetworkConfigurationPolicy
metadata:
 name: br1-eth2-policy
spec:
 nodeSelector:
   external-network: "true"
 desiredState:
   interfaces:
     - name: br1
       description: Linux bridge with eth2 as a port
       type: linux-bridge
       state: up
       ipv4:
         dhcp: true
         enabled: true
       bridge:
         options:
           stp:
             enabled: false
         Port:
           - name: eth2

Pero antes de crear el manifiesto anterior, como se puede observar, la política se asocia a nodos a través de un nodeSelector, por lo que vamos a etiquetar nuestro nodo antes de crear la política.

Para ello, ejecutamos:

oc label node external-network='true' -l node-role.kubernetes.io/worker=''

Y ahora creamos el objeto con el manifiesto anterior a través de un fichero o por la interfaz web.

oc create -f br1-eth2-policy.yaml

Una vez creada podemos ver el estado de la política con en siguiente comando:

$ oc get nncp
NAME                                           STATUS
br1-eth2-policy                                progressing

Y ver el estado de la NodeNetworkConfigurationEnactment creada automáticamente por la política con el comando:

$ oc get nnce
NAME                                           STATUS
ip-10-0-179-230.eu-west-1.compute.internal.br1-eth2-policy   Available

Deberemos esperar hasta que ambos objetos estén en status available.

NNS y Node Allocatable

Una vez los objetos anteriores están disponibles podemos mirar el NNS y ver si se ha creado el nuevo bridge:

apiVersion: nmstate.io/v1beta1
kind: NodeNetworkState
metadata:
  name: ip-10-0-179-230.eu-west-1.compute.internal
status:
  currentState:
    interfaces:
    - bridge:
        options:
          group-addr: 01:80:C2:00:00:00
          group-forward-mask: 0
          hash-max: 4096
          mac-ageing-time: 300
          multicast-last-member-count: 2
          multicast-last-member-interval: 100
          multicast-querier: false
          multicast-querier-interval: 25500
          multicast-query-interval: 12500
          multicast-query-response-interval: 1000
          multicast-query-use-ifaddr: false
          multicast-router: 1
          multicast-snooping: true
          multicast-startup-query-count: 2
          multicast-startup-query-interval: 3125
          stp:
            enabled: false
            forward-delay: 15
            hello-time: 2
            max-age: 20
            priority: 32768
        port:
        - name: eth2
          stp-hairpin-mode: false
          stp-path-cost: 100
          stp-priority: 32
          vlan:
            enable-native: false
            mode: trunk
            trunk-tags:
            - id-range:
                max: 4094
                min: 2
      description: Linux bridge with eth2 as a port
      ipv4:
        address:
        - ip: 192.168.0.60
          prefix-length: 24
        auto-dns: true
        auto-gateway: true
        auto-route-table-id: 0
        auto-routes: true
        dhcp: true
        enabled: true
        lldp:
        enabled: false
      mac-address: 0A:AE:69:A1:16:FF
      mtu: 9001
      name: br1
      state: up
      type: linux-bridge
    - ipv4:
        address:
        - ip: 10.0.179.230
          prefix-length: 17
        auto-dns: true
        auto-gateway: true
        auto-route-table-id: 0
        auto-routes: true
        dhcp: true
        enabled: true
       lldp:
        enabled: false
      mac-address: 0A:FA:D4:3D:B0:A7
      mtu: 9001
      name: ens6
      state: up
      type: ethernet

    - ipv4:
        address: []
        dhcp: false
        enabled: false
      ipv6:
        address: []
        autoconf: false
        dhcp: false
        enabled: false
      lldp:
        enabled: false
      mac-address: 0A:AE:69:A1:16:FF
      mtu: 1500
      name: eth2
      state: up
      type: ethernet
    - ipv4:
        address:
        - ip: 127.0.0.1
          prefix-length: 8
        enabled: true
      ipv6:
        address:
        - ip: ::1
          prefix-length: 128
        enabled: true
      mac-address: "00:00:00:00:00:00"
      mtu: 65536
      name: lo
      state: up
      type: unknown

Una vez confirmado el bridge, debemos ver si el nodo de Kubernetes detecta este bridge como disponible para ser utilizado. Para ello ejecutamos el siguiente comando:

$ oc get no -o json | jq '.items[].status.allocatable'
{
 "attachable-volumes-aws-ebs": "25",
 "bridge.network.kubevirt.io/br1": "1k",
 "cpu": "71500m",
 "devices.kubevirt.io/kvm": "1k",
 "devices.kubevirt.io/sev": "0",
 "devices.kubevirt.io/tun": "1k",
 "devices.kubevirt.io/vhost-net": "1k",
 "ephemeral-storage": "96143180846",
 "hugepages-1Gi": "0",
 "hugepages-2Mi": "0",
 "memory": "196537932Ki",
 "ovs-cni.network.kubevirt.io/br0": "1k",
 "pods": "250"
}

Vemos como el bridge que acabamos de crear está disponible en la línea "bridge.network.kubevirt.io/br1": "1k". Deberemos guardar esta referencia para utilizar posteriormente con la networkAttachmentDefinition.

NetworkAttachmentDefinition

Utilizando la referencia anterior podríamos crear una network attachment definition de la siguiente manera:

apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  annotations:
    k8s.v1.cni.cncf.io/resourceName: bridge.network.kubevirt.io/br1
  name: br1-eth2-net
  namespace: default
spec:
  config: >-
    {"name":"br1-eth2-net","type":"cnv-bridge","cniVersion":"0.3.1","bridge":"br1","macspoofchk":false,"ipam":{}}

Si la creamos así, deberemos gestionar la configuración de IPs separadamente.
Con las network attachment definitions, es posible configurar que las interfaces añadidas soliciten IP vía DHCP si tuviésemos configurado un servidor DHCP en la red adicional usando:

{"name":"br1-eth2-net","type":"cnv-bridge","cniVersion":"0.3.1","bridge":"br1","macspoofchk":false,"ipam":{"type" : "dhcp"}}

O incluso añadir IPs estáticas, gateways, routes, etc.:

{"name":"br1-eth2-net","type":"cnv-bridge","cniVersion":"0.3.1","bridge":"br1","macspoofchk":false,"ipam":{"type": "static", "addresses": [ { "address": "192.168.0.61/26" }] }}

Deberemos tener esto en cuenta a la hora de configurar las IPs en VMs y Pods.

Como no tenemos disponible un DHCP en la red adicional y para demostrar las capacidades de las net-attach-def, crearemos 2 objetos.

apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  annotations:
    k8s.v1.cni.cncf.io/resourceName: bridge.network.kubevirt.io/br1
  name: net-br1-pod
spec:
  config: >-
    {"name":"net-br1-pod","type":"cnv-bridge","cniVersion":"0.3.1","bridge":"br1","macspoofchk":true,"ipam":{"type":"static","addresses":
    [ { "address": "192.168.0.61/24" }] }}
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  annotations:
    k8s.v1.cni.cncf.io/resourceName: bridge.network.kubevirt.io/br1
  name: net-br1-vm
spec:
  config: >-
    {"name":"net-br1-pod","type":"cnv-bridge","cniVersion":"0.3.1","bridge":"br1","macspoofchk":true,"ipam":{"type":"static","addresses":
    [ { "address": "192.168.0.62/24" }] }}

Recordemos: si tuviésemos un DHCP, bastaría con adjuntar pod y vm a la misma net-attach-def indicando que el IPAM es de tipo DHCP.

Configuración VM y Pod

Es la hora de añadir la red que hemos configurado a VMs y Pods.

VM

Para añadir una red adicional en una VM, debemos asociar la net-attach-def que creamos antes con una nueva interfaz de red en una Máquina Virtual apagada de OpenShift Virtualization.

Podemos hacerlo a través del manifiesto de la VM o de la consola web, en este caso usaremos la consola.

Manifiesto de la VM o de la consola web
Añadir Network Interface.

Una vez añadida, deberíamos configurar qué IP queremos asignar a la VM, si no se ha hecho a través de la net-attach-def.

Como la hemos hecho a través de la NAD no hay más que hacer, pero podríamos configurar la IP de la VM usando cloud-init modificando el manifiesto de la VM.

kind: VirtualMachine
spec:
# ...
  template:
  # ...
    spec:
      volumes:
      - cloudInitNoCloud:
          networkData: |
            version: 2
            ethernets:
              eth1: 
                addresses:
                - 10.10.10.14/24

O gestionando, por ejemplo, a través de ConfigMaps o cloud-init que exista un fichero de este estilo en la ruta /etc/sysconfig/network-scripts/ifcfg-eth1.

TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=none
IPADDR=192.168.0.62
PREFIX=26
DEFROUTE=no
IPV4_FAILURE_FATAL=no
IPV6_DISABLED=yes
IPV6INIT=no
NAME=eth1
UUID=bcc1d1f7-93c1-3029-9183-260631483bdd
DEVICE=eth1
ONBOOT=yes
AUTOCONNECT_PRIORITY=-999
HWADDR=00:00:00:00:00:02

Confirmamos que la VM tiene la IP que deseamos:

IP de la VM

Pod

Para crear un pod con una IP en una red adicional, en este caso al haber usado la NAD para gestionar la IP es tan simple como crear el siguiente manifiesto:

apiVersion: v1
kind: Pod
metadata:
  name: samplepod
  annotations:
    k8s.v1.cni.cncf.io/networks: '[{
      "name": "net-br1-pod",
      "default-route": ["192.168.0.2"]
    }]'
spec:
  containers:
  - name: samplepod
    command: ["/bin/ash", "-c", "trap : TERM INT; sleep infinity & wait"]
    image: alpine

Lo confirmamos entrando en el pod y viendo las interfaces de red y viendo que además de la red de pods existe otra interfaz con la ip que definimos en la NAD:

Conectando a Samplepod

Test

Comprobaremos que podemos alcanzar recursos en la nueva red usando simplemente ping.

Ping desde el Pod a la VM:

Ping desde el Pod a la VM

Ping desde la VM al pod:

Ping desde la VM al Pod

Conclusión

Añadiendo redes adicionales a la red de Pods en Kubernetes, se abre un abanico de posibilidades para solventar requerimientos y desafíos complejos en nuestros clusters. En estos casos, Multus CNI es nuestro mejor aliado.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.