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.