Cloud : un peu d’ARM avec votre cluster Kubernetes ?
Tags: cloud, kubernetes, arm, docker
Published: 2024-05-06 12:10:04

La majorité des clouds providers proposent des solutions basées sur des architectures processeurs **ARM**, tel que **Graviton** chez AWS ou **Tau T2A** chez GCP. Les avantages de tels processeurs sont multiples : efficacité énergétique, couts réduits, performances… Ils sont de plus tout à fait adaptés aux environnements conteneurisés.

Exécuter vos workloads **Kubernetes** sur des processeurs ARM parait donc être un bonne idée. Cela rentre aussi dans une approche **FinOps**, car l’utilisation de processeurs arm en lieu et place de processeurs x86 va permettre des réductions de coûts non négligeables (de l’ordre de 20% avec des processeurs Graviton) à performances égales voire supérieures. 

Si la mise en place d’une architecture arm peut paraitre assez simple sur de nouveaux clusters, qu’en est-il de la migration d’une architecture existante amd64 vers une architecture arm64 ? 

## Considérations

Avant de se lancer dans la migration vers une architecture arm, quelques considérations sont à prendre en compte vis-à-vis des applications qui tournent sur votre cluster.

### Langages et librairies

La majorité des langages supportent maintenant des architectures ARM. Les langages interprétés (NodeJS, Python…) ou byte-code compilés (Java, .Net) devraient fonctionner sans modifications majeures. Attention cependant si vous utilisez des librairies ou fragments de codes natifs (JNI), une recompilation sera necessaire.

Les langages compilés (C/C++, Go…) supportent pour la plus grande majorité les architectures ARM mais ils devront être recompilés.

### Images Docker

De manière générale nos applications packagées pour s’executer dans des conteneurs utilisent une image de base (le **FROM** du Dockerfile). Attention à bien vérifier que cette image aussi supporte ARM. C’est le cas de la majorité des standards et cela peut se vérifier rapidement en se connectant sur le registry depuis lequel elles sont tirées. Par exemple pour l’image officielle OpenJDK sur Dockerhub, on remarque que les 2 types d’architectures sont bien supportées :

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y8fi4wv0uo9wz70y4jg5.png)

### Services tiers

Des services tiers tels que Prometheus ou ArgoCD peuvent aussi tourner sur nos cluster Kubernetes afin d’assurer diverses taches (observabilité, déploiement, sécurité…). Il faudra donc s’assurer là aussi que ces services sont déployables sur une architecture ARM.

### Système d’exploitation

Si vous utilisez des services cloud entièrement managés tel que **EKS Fargate** ou **GKE Autopilot** il n’y aura aucun impact. Par contre si vous avez des noeuds que vous managez vous-même, une migration du système d’exploitation sera nécessaire. 

## Construction : Docker multi-architecture

Maintenant que vous vous êtes assuré que vos applications sont bien éligibles à une plateforme arm, il s’agit de les reconstruire afin qu’elles puissent tourner sur ce type d’architecture. 

### Principe

La meilleure solution pour pouvoir instancier vos conteneurs sur une architecture ARM n’est pas d’effectuer un build de vos images pour ce type d’architecture spécifiquement, mais plutôt d’utiliser une méthode de build **multi-architecture**. Votre image sera alors construite en même temps et à partir de la même source (DockerFile) pour une liste d’architecture que vous aurez prédéfinie.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17q049m1rluqkb1ra10k.png)

Cela permettra de déployer ces nouvelles images sur vos clusters nouvellement configurés avec des noeuds arm, mais aussi de pouvoir lancer indifféremment vos containers sur des architectures plus classiques type amd64 pour du développement ou des tests par exemple.

Ce sera le runtime de conteneur qui, au moment du pull, récupérera les layers de l’image correspondant au type d’architecture sur lequel il tourne : 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tadeyrq9mbtg592hqf3j.png)

### Mise en place

### Solution 1 : Docker buildx

Une approche courante pour construire des images Docker multi-architecture consiste à utiliser le plugin docker **buildx** (https://docs.docker.com/build/architecture/#buildx). 

Ce plugin se base sur **QEMU** (Quick Emulator) pour construire des images multi-architectures. QEMU est un émulateur de processeur qui permet d'exécuter du code destiné à une architecture spécifique depuis un autre type d’architecture. Cela va permettre de construire des images Docker pour des architectures différentes de celle de l'hôte.

Concrètement, tout ce que vous aurez à faire est d’installer le plugin docker buildx (vérifier la compatibilité avec votre version de docker), et de lancer un build en listant les plateformes cibles :

```bash
docker buildx create --use --name mybuild node-amd64
mybuild
docker buildx create --append --name mybuild node-arm64
docker buildx build --platform linux/amd64,linux/arm64 .
```

Le build et le push de l’image peuvent se faire avec une seule et même instruction :

```bash
docker buildx build --tag my-user/my-image --platform linux/arm64/v8,linux/amd64 --push .
```

Pour plus de détails vous pouvez vous référer à la procédure Docker: https://docs.docker.com/build/building/multi-platform/

### Solution 2 : Manisfest

Une seconde solution, plus complexe, est la méthode “Do It Yourself”. Elle consiste à une création manuelle du manifest d'image après avoir effectué 2 builds, un pour chaque type d’architecture.

Elle fait aussi intervenir 3 registres d’images, car chacune des images construites doit être poussée dans son propre registre avant d’être poussée une 3ème fois dans un registre multi-architecture.

Donc pour résumer :

1 - On construit et on pousse une image pour chaque architecture

```bash
# AMD64
$ docker build -t my-user/my-image-amd64 --build-arg ARCH=amd64/ .
$ docker push my-user/my-image-amd64

# ARM64V8
$ docker build -t my-user/my-image-arm --build-arg ARCH=arm64v8/ .
$ docker push my-user/my-image-arm
```

 2 - On créé un manifeste à partir de chacune des images

```bash
docker manifest create \
my-user/my-image \
--amend my-user/my-image-amd64 \
--amend my-user/my-image-arm
```

3 - On pousse le nouveau manifest

```bash
docker manifest push my-user/my-image
```

_Remarque_ : il est aussi possible de jouer sur les tags des différentes images pour n’utiliser qu’un seul registry

Cette solution peut être utile si vous construisez vos images avec un autre outils que Docker, tels que Kaniko ou Buildah. 

Un post sur le blog de Docker détaille ces 2 méthodes: https://www.docker.com/blog/multi-arch-build-and-images-the-simple-way/

## Hosting : Cluster Kubernetes Hybride

Vous avez maintenant des images Docker multi-architectures capable de tourner indifféremment sur de l’amd64 ou de l’arm. Mais il est possible que pour une raison ou une autre, certaines de vos applications n’aient pas pu être construites pour architecture arm et que vous deviez donc conserver des noeuds type amd64 pour celles-ci.

Dans ce cas pas de panique, vous pouvez jouer sur les teintes et node selector (ou node affinity) de Kubernetes. 

Je m’explique. Les clusters Kubernetes managés sont capables de gérer plusieurs groupes de noeuds, chacun de ces groupes pouvant s’appuyer sur des propriétés différentes (type d’instance, nombre d’instances…). Il est alors tout à fait possible de créer 2 groupes de noeuds distincts, l’un comportant des machines de type amd64 et l’autre de type arm. Pour garder la maitrise sur les workloads qui vont être déployés par la suite sur l’un ou l’autre groupe de noeuds, on appliquera une teinte sur l’un des groupes :

(Si vous n’êtes pas familier avec la notion de teinte et de node selector je vous invite à consulter à la documentation Kubernetes https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ et https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7nvrvr5r7nic0d81a4zk.png)

Par défaut tous nos pods seront ainsi déployés sur le node group arm. On utilisera alors un node selector ainsi qu’une tolération au niveau des pods que l’on souhaite assigner à un type d’architecture amd64. 

```yaml
tolerations:
- effect: NoSchedule
  key: node-arch-type
  value: amd64
nodeSelector:
  kubernetes.io/arch: amd64
```

Cela permet de continuer de faire tourner sans risque vos applications nos compatibles arm sur des noeuds amd64.

_Remarque_: on peut tout à fait envisager le mécanisme inverse, à savoir un déploiement par défaut sur des noeuds type amd64 (non teintés) et prévoir une sélection d’applications à déployer sur des noeuds type arm (grâce aux teintes et node selectors). Cela permet dans un cluster existant amd64, d’envisager une stratégie de migration de vos applications par lot.

## Conclusion

**Félicitations !** Vous avez maintenant un cluster Kubernetes capable d’héberger plusieurs types d’architectures, et des applications pouvant se déployer indifféremment sur l’une ou l’autre tout en gérant vous même cette répartition.

Ce type d'architecture de plus en plus répandu mérite vraiment de s'y intéresser, surtout dans le cas de la mise en place d'une nouvelle plateforme. 

Pour ce qui est de la migration d'une plateforme existante, le ROI est de manière générale très intéressant mais une stratégie de migration doit impérativement être mise en place.