Guide de développement des contrôleurs ICN et SD-EWAN pour Kubernetes

Architecture ICN et SD-EWAN

Le projet ICN (Integrated Cloud Native) vise à résoudre les défis liés aux déploiements en périphérie de réseau (edge computing). Son composant central, SD-EWAN (Software Defined Edge WAN), gère la connectivité réseau entre plusieurs clusters edge ou entre un cluster edge et l'internet public. L'architecture repose sur des fonctions réseau cloud-native (CNF) et un hub de contrôle centralisé pour orchestrer le trafic.

Préparation de l'environnement de test

Pour expérimenter avec SD-EWAN, il est nécessaire de provisionner un environnement composé de plusieurs nœuds. L'outil Vagrant permet d'automatiser la création de machines virtuelles simulant des clusters edge et un hub central.

# Installation des dépendances et de Vagrant sur l'hôte
sudo apt update && sudo apt install -y libvirt-daemon-system qemu-kvm dnsmasq
sudo apt install -y vagrant
vagrant plugin install vagrant-libvirt

# Initialisation des machines virtuelles
git clone https://gerrit.akraino.org/r/icn
cd icn
vagrant up
vagrant ssh

Une fois connecté à la machine virtuelle, l'installation de Docker et de Kubeadm est requise pour initialiser le cluster Kubernetes.

# Configuration du daemon Docker
sudo mkdir -p /etc/systemd/system/docker.service.d
cat <<EOF | sudo tee /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "storage-driver": "overlay2"
}
EOF
sudo systemctl daemon-reload && sudo systemctl restart docker

# Installation de Kubeadm et désactivation du swap
sudo swapoff -a
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

# Initialisation du cluster
NODE_IP=$(ip route get 1 | awk '{print $7; exit}')
sudo kubeadm init --pod-network-cidr 10.244.0.0/16 --apiserver-advertise-address=$NODE_IP
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config

Déploiement des plugins réseau (OVN et Multus)

Le réseau du cluster est géré par OVN (Open Virtual Network) et Multus pour permettre l'attachement de multiples interfaces réseau aux pods.

# Déploiement de Multus CNI
kubectl apply -f https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/master/deployments/multus-daemonset.yml

# Configuration d'OVN4NFV
cat <<EOF | kubectl apply -f -
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: edge-network-attachment
spec:
  config: '{
     "cniVersion": "0.3.1",
     "name": "edge-ovn-plugin",
     "type": "ovn4nfvk8s-cni"
  }'
EOF

Développement de CRD avec Kubebuilder

L'extension des capacités de Kubernetes pour gérer les interfaces réseau SD-EWAN nécessite la création de Définitions de Ressources Personnalisées (CRD). Kubebuilder est l'outil de référence pour générer le scaffolding du contrôleur.

# Installation de Kubebuilder
curl -L -o kubebuilder https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)
chmod +x kubebuilder && sudo mv kubebuilder /usr/local/bin/

# Initialisation du projet
mkdir sdewan-controller && cd sdewan-controller
go mod init github.com/edge-network/sdewan-controller
kubebuilder init --domain edge-network.io
kubebuilder create api --group network --version v1beta1 --kind EdgeInterface

make install

Extension de l'API OpenWrt via Lua

Les CNF SD-EWAN s'exécutent souvent sur OpenWrt. Pour exposer les métriques réseau, un point de terminaison REST est ajouté via le serveur web uHTTPd en utilisant Lua.

module("luci.controller.edge_metrics", package.seeall)

local sys = require "luci.sys"
local json = require "luci.jsonc"

function index()
    entry({"api", "v1", "metrics"}, call("fetch_network_metrics")).dependent = false
end

function parse_interface_data(raw_data)
    local metrics = {}
    for line in raw_data:gmatch("[^\r\n]+") do
        local iface = line:match("^(%S+):")
        if iface and iface ~= "lo" then
            metrics[iface] = {
                status = line:match("UP") and "active" or "inactive",
                mtu = line:match("mtu (%d+)")
            }
        end
    end
    return metrics
end

function fetch_network_metrics()
    local raw_output = sys.exec("ip -o link show")
    local parsed_data = parse_interface_data(raw_output)
    
    luci.http.prepare_content("application/json")
    luci.http.write(json.stringify(parsed_data))
end

Implémentation du Client Go pour OpenWrt

Le contrôleur Kubernetes doit communiquer avec l'API OpenWrt. Un client Go dédié est implémenté pour récupérer l'état des interfaces.

package openwrt

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type NetworkManager struct {
	BaseURL    string
	HTTPClient *http.Client
}

type InterfaceMetrics struct {
	Status string `json:"status"`
	MTU    string `json:"mtu"`
}

func NewNetworkManager(podIP string) *NetworkManager {
	return &NetworkManager{
		BaseURL:    fmt.Sprintf("http://%s/api/v1/metrics", podIP),
		HTTPClient: &http.Client{},
	}
}

func (nm *NetworkManager) FetchMetrics() (map[string]InterfaceMetrics, error) {
	resp, err := nm.HTTPClient.Get(nm.BaseURL)
	if err != nil {
		return nil, fmt.Errorf("failed to query openwrt api: %w", err)
	}
	defer resp.Body.Close()

	var metrics map[string]InterfaceMetrics
	if err := json.NewDecoder(resp.Body).Decode(&metrics); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}
	return metrics, nil
}

Logique de Réconciliation du Contrôleur

Le cœur du contrôleur réside dans la fonction Reconcile. Elle observe l'état désiré de la CRD EdgeInterface et met à jour le statut en interrogeant les pods CNF correspondants.

func (r *EdgeInterfaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("edgeinterface", req.NamespacedName)

	var targetInterface networkv1beta1.EdgeInterface
	if err := r.Get(ctx, req.NamespacedName, &targetInterface); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// Sélection des pods CNF basés sur les labels
	podList := &corev1.PodList{}
	matchLabels := targetInterface.Spec.Selector.MatchLabels
	if err := r.List(ctx, podList, client.MatchingLabels(matchLabels)); err != nil {
		log.Error(err, "unable to list target pods")
		return ctrl.Result{}, err
	}

	// Agrégation des métriques depuis les pods
	for _, pod := range podList.Items {
		manager := openwrt.NewNetworkManager(pod.Status.PodIP)
		metrics, err := manager.FetchMetrics()
		if err != nil {
			log.Error(err, "failed to fetch metrics from pod", "pod", pod.Name)
			continue
		}

		if ifaceData, exists := metrics[targetInterface.Spec.InterfaceName]; exists {
			targetInterface.Status.CurrentState = ifaceData.Status
			targetInterface.Status.CurrentMTU = ifaceData.MTU
			break
		}
	}

	if err := r.Status().Update(ctx, &targetInterface); err != nil {
		log.Error(err, "unable to update EdgeInterface status")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

Configuration des Webhooks d'Admission et RBAC

Pour valider les ressources avant leur persistance dans etcd, un webhook d'admission est déployé. Cela nécessite la génération de certificats TLS et la configuration des autorisations RBAC.

# Génération des certificats pour le webhook
openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \
    -subj "/CN=webhook-service.kube-system.svc" \
    -keyout /tmp/tls.key -out /tmp/tls.crt

# Création du secret TLS dans le cluster
kubectl create secret tls webhook-certs \
    --cert=/tmp/tls.crt --key=/tmp/tls.key \
    -n kube-system

# Configuration RBAC pour l'opérateur
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: edge-network-manager
rules:
- apiGroups: ["network.edge-network.io"]
  resources: ["edgeinterfaces"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: edge-network-manager-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: edge-network-manager
subjects:
- kind: ServiceAccount
  name: default
  namespace: kube-system
EOF

Concepts Avancés de controller-runtime

Lors du développement de contrôleurs complexes, la distinction entre Owns et Watches est cruciale. La méthode Owns est utilisée pour les ressources directement créées et gérées par le contrôleur parent, déclenchant une réconciliation du propriétaire lors de modifications. À l'inverse, Watches permet de surveiller des ressources externes et d'utiliser des fonctions de mappage personnalisées via EnqueueRequestsFromMapFunc.

// Exemple de fonction de mappage pour déclencher des réconciliations croisées
func MapNodeToEdgeInterfaces(obj client.Object) []reconcile.Request {
	node, ok := obj.(*corev1.Node)
	if !ok {
		return nil
	}

	var requests []reconcile.Request
	// Logique pour trouver les EdgeInterfaces associées à ce nœud
	// et les ajouter à la file d'attente de réconciliation
	for _, label := range node.Labels {
		requests = append(requests, reconcile.Request{
			NamespacedName: types.NamespacedName{
				Name:      label,
				Namespace: "default",
			},
		})
	}
	return requests
}

// Enregistrement dans le builder du contrôleur
ctrl.NewControllerManagedBy(mgr).
	For(&networkv1beta1.EdgeInterface{}).
	Watches(
		&source.Kind{Type: &corev1.Node{}},
		handler.EnqueueRequestsFromMapFunc(MapNodeToEdgeInterfaces),
	).
	Complete(r)

L'utilisation de SharedInformers en arrière-plan permet de maintenir un cache local des ressources, réduisant ainsi la charge sur l'API server. Les paniques liées à l'assignation dans des maps nil lors de l'utilisaiton de EnqueueRequestsFromMapFunc surviennent généralement lorsque le cache de l'informateur n'est pas correctement initialisé ou synhcronisé avant l'exécution de la fonction de mappage.

Étiquettes: kubernetes Kubebuilder SD-EWAN OpenWrt OVN

Publié le 7 juin à 00h27