Tutoriel : Comment installer un cluster Kubernetes Exoscale SKS avec Terraform ?

Publié le

Dans un précédant article, nous avons eu un aperçu de l'offre Kubernetes managée d'Exoscale (alias SKS pour Scalable Kubernetes Service). Nous avions abordé une méthode d'installation de SKS au moyen de l'outil en ligne de commande exo. Cette installation s'est révélée plutôt simple. Cependant il peut être utile de mettre en place ce service au moyen d'outils d'Infrastructure as Code.

Ici, nous allons reprendre un cheminement proche de celui du précédant article, mais au lieu d'utiliser l'outil exo, nous allons le faire avec Terraform.

Prérequis

Nous aurons uniquement besoin de Terraform. Ce tutoriel repose sur le provider officiel Exoscale. Celui-ci sera téléchargé automatiquement par Terraform lors du processus d'initialisation de l'infrastructure. Le provider d'Exoscale permet de réserver des clusters et des pools de nœuds depuis sa version 1.22.

Composition de l'infrastructure

Notre infrastructure va implémenter les éléments suivants :

  • des règles de pare-feu (security group) communes aux nœuds du cluster,
  • la partie control-plane gérée par Exoscale (resource de type exoscale_sks_cluster)

Pour chaque pool de nœuds, nous définissons :

  • une règle d'anti-affinity (exoscale_affinity) pour favoriser la haute disponibilité, en évitant que les workers soient tous déployés sur le même hyperviseur
  • un pool à proprement parler (ressource de type exoscale_sks_nodepool)

Préparation du code de l'infrastructure

Nous définissons le provider dans terraform/providers.tf :

provider "exoscale" {
  timeout = 120
}

terraform {
  required_providers {
    exoscale = {
      source = "exoscale/exoscale"
    }
  }
  required_version = ">= 0.13"
}

Nous créons ensuite un fichier terraform/variables.tf. Celui-ci contient quelques options de configuration pertinantes :

variable "zone" {
  default = "ch-gva-2"
}

variable "name" {
  default = "kubernetes"
}

variable "kubernetes_version" {
  default = "1.20.2"
}

variable "service_level" {
  default = "pro"
}

variable "pools" {
  type = map(map(string))
  default = {
    "pool-1" = { instance_type = "medium", size = 1 },
    "pool-2" = { instance_type = "medium", size = 1 },
    "pool-3" = { instance_type = "medium", size = 1 },
  }
}

Les noms de ces variables zone, name, et kubernetes_version parlent d'eux même.

La variable service_level permet de définir le niveau de l'offre SKS. Si elle est définie à starter, la partie control-plane du cluster est gratuite.

Dans ce tutoriel, nous choisissons de définir la variable service_level à pro. Quand Exoscale SKS sera GA, ce niveau de service sera payant (0,05€/heure), et présentera les avantages suivants par rapport à l'offre starter :

  • control-plane en "Haute disponibilité"
  • sauvegardes quotidiennes d'Etcd, qui stocke l'état du cluster, ce qui est un atout pour mettre en place un "Disaster Recovery Plan" plus efficace
  • un SLA raisonnable de 99,95%

La variable pools permet de définir et redimenssioner plusieurs pools de nœuds très facilement. Ici tous les pools sont définis avec une seule machine virtuelle.

Nous pouvons ensuite passer à la définition de l'infrastructure, dans le fichier terraform/main.tf.

Nous commençons par définir les règles de pare-feu des workers. Cette définition se fait avec des ressources exoscale_security_group et exoscale_security_group_rules. Nous n'autorisons que les ports suivants :

  • 30000 à 32767 en TCP et UDP pour permettre d'exposer des services de type NodePort (donc aussi nécessaires aux services de type LoadBalancer)
  • 10250 en TCP pour permettre le fonctionnement de kubectl logs et kubectl exec
  • 4789 en UDP pour que Calico fonctionne. Calico est le plugin CNI déployé dans Exoscale SKS. Cette règle n'est autorisée que depuis la security-group elle même
resource "exoscale_security_group" "pools" {
  name        = format("%s-pools", var.name)
  description = format("%s security group", var.name)
}

resource "exoscale_security_group_rules" "pools" {
  security_group_id = exoscale_security_group.pools.id

  ingress {
    protocol    = "TCP"
    ports       = ["30000-32767"]
    cidr_list   = ["0.0.0.0/0", "::/0"]
    description = "NodePort TCP services"
  }

  ingress {
    protocol    = "UDP"
    ports       = ["30000-32767"]
    cidr_list   = ["0.0.0.0/0", "::/0"]
    description = "NodePort UDP services"
  }

  ingress {
    protocol    = "TCP"
    ports       = ["10250"]
    cidr_list   = ["0.0.0.0/0", "::/0"]
    description = "Kubelet pods logs"
  }

  ingress {
    protocol                 = "UDP"
    ports                    = ["4789"]
    user_security_group_list = [exoscale_security_group.pools.name]
    description              = "Calico internal traffic"
  }
}

Cette définition de pare-feu est utilisée pour tous les nœuds de travail du cluster.

Nous pouvons ensuite définir la partie control-plane, qui est gérée par Exoscale :

resource "exoscale_sks_cluster" "cluster" {
  zone          = var.zone
  name          = var.name
  version       = var.kubernetes_version
  service_level = var.service_level
}

D'autres options pourraient être ajoutées, la liste complète est disponible dans la documentation du provider.

Nous pouvons ensuite définir les pools de nœuds et leur règle d'anti-affinity :

resource "exoscale_affinity" "pool" {
  for_each = var.pools

  name = format("%s-%s", var.name, each.key)
  type = "host anti-affinity"
}

resource "exoscale_sks_nodepool" "pool" {
  for_each = var.pools

  zone          = var.zone
  cluster_id    = exoscale_sks_cluster.cluster.id
  name          = each.key
  instance_type = each.value.instance_type
  size          = each.value.size

  anti_affinity_group_ids = [exoscale_affinity.pool[each.key].id]
  security_group_ids      = [exoscale_security_group.pools.id]
}

Création de l'infrastructure

Pour information, le code complet du projet est disponible sur GitHub.

Nous allons créer mainenant l'infrastructure :

export EXOSCALE_API_KEY=EXOxxxxxxxxxxxxxxxxxxxxxxxx
export EXOSCALE_API_SECRET=my-secret

terraform init
terraform apply

Pour voir comment récupérer une paire de clés d'API, il est possible de se référer à cette page.

Après confirmation et moins de 2 minutes d'exécution, l'infrastructure est réservée.

Dans l'interface de gestion d'Exoscale, nous retrouvons bien les éléments de notre infrastructure, notamment les pools :

2021-03-09-Exoscale-sks-node-pools.jpg

Depuis l'outil exo :

exo sks show kubernetes -z ch-gva-2
┼───────────────┼──────────────────────────────────────────────────────────────────┼
│  SKS CLUSTER  │                                                                  │
┼───────────────┼──────────────────────────────────────────────────────────────────┼
│ ID            │ 12cdd5e5-e9f0-4916-9789-e259c147be8b                             │
│ Name          │ kubernetes                                                       │
│ Description   │                                                                  │
│ Zone          │ ch-gva-2                                                         │
│ Creation Date │ 2021-03-07 21:47:50 +0000 UTC                                    │
│ Endpoint      │ https://12cdd5e5-e9f0-4916-9789-e259c147be8b.sks-ch-gva-2.exo.io │
│ Version       │ 1.20.2                                                           │
│ Service Level │ pro                                                              │
│ CNI           │ calico                                                           │
│ Add-Ons       │ exoscale-cloud-controller                                        │
│ State         │ running                                                          │
│ Nodepools     │ b16275af-bb70-47ea-8b7f-31396381123c | pool-1                    │
│               │ 0563e934-f2e7-48ce-93e6-e30106fc6d79 | pool-2                    │
│               │ 6118a970-179a-4448-ab57-72856db2e27c | pool-3                    │
┼───────────────┼──────────────────────────────────────────────────────────────────┼

Récupération d'une configuration (kubeconfig)

Pour utiliser le cluster Kubernetes, il reste à créer une configuration (kubeconfig). Cela se fait avec l'outil exo :

mkdir -p $HOME/.kube
exo sks kubeconfig kubernetes admin -z ch-gva-2 -g system:masters -t $((86400 * 7)) > $HOME/.kube/config

L'option -t (TTL) permet de définir une durée de vie maximale de validité pour cette configuration (ici il est de 7 jours).
Un test rapide nous permet de voir la situation du cluster fraîchement provisionné :

kubectl get pods,svc -o wide --all-namespaces

Voici le résultat :

NAMESPACE     NAME                                          READY   STATUS    RESTARTS   AGE     IP                NODE               NOMINATED NODE   READINESS GATES
kube-system   pod/calico-kube-controllers-86bddfcff-4kxq8   1/1     Running   0          8m37s   192.168.47.132    pool-3b74f-fvopf   <none>           <none>
kube-system   pod/calico-node-c7k6q                         1/1     Running   0          5m36s   159.100.243.86    pool-3b74f-fvopf   <none>           <none>
kube-system   pod/calico-node-hwtr2                         1/1     Running   0          5m24s   159.100.243.2     pool-5a103-sawzv   <none>           <none>
kube-system   pod/calico-node-srxds                         1/1     Running   0          5m32s   159.100.241.105   pool-02d85-atvtw   <none>           <none>
kube-system   pod/coredns-56bd9b9785-mqktf                  1/1     Running   0          8m37s   192.168.47.131    pool-3b74f-fvopf   <none>           <none>
kube-system   pod/konnectivity-agent-754b65d547-n57lc       1/1     Running   0          8m36s   192.168.47.129    pool-3b74f-fvopf   <none>           <none>
kube-system   pod/konnectivity-agent-754b65d547-p769t       1/1     Running   0          8m37s   192.168.47.130    pool-3b74f-fvopf   <none>           <none>
kube-system   pod/kube-proxy-5m26p                          1/1     Running   0          5m16s   159.100.243.86    pool-3b74f-fvopf   <none>           <none>
kube-system   pod/kube-proxy-b54d6                          1/1     Running   0          5m12s   159.100.241.105   pool-02d85-atvtw   <none>           <none>
kube-system   pod/kube-proxy-sp857                          1/1     Running   0          5m4s    159.100.243.2     pool-5a103-sawzv   <none>           <none>

NAMESPACE     NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE     SELECTOR
default       service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  9m41s   <none>
kube-system   service/kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   9m3s    k8s-app=kube-dns

Note : pour le moment, le provider Terraform ne permet pas de générer automatiquement des fichiers kubeconfig. Personnellement, je trouve que c'est une bonne chose, ça évite que des informations sensibles ne se retrouvent dans l'état de l'infrastructure (fichier .tfstate ou autre backend ne supportant pas le chiffrement). La gestion des configurations pourrait être gérée de façon sécurisée avec IAM, Vault, et un petit script personnalisé par exemple. Le tout, avec un TTL assez court, à la fois sur le kubeconfig, et sur le token d'API généré par l'IAM, s'il vous plait 😊. (Nous aborderons peut être cette façon de faire dans un autre article.)

Destruction de l'infrastructure

Comme toujours avec Terraform, si vous n'avez plus besoin de l'infrastructure, vous pouvez la supprimer avec la commande :

terraform destroy

Conclusion

Nous constatons au travers de ce tutoriel que chaque définition de ressources avec Terraform correspond à un appel à l'outil exo, vu dans le précédant article sur SKS.

Les deux outils permettent d'arriver à un résultat similaire, mais pour la création d'une infrastructure évolutive, nous préférerons utiliser Terraform, qui permet l'implémentation d'Infrasturcture as Code.

Avec à peine une centaine de lignes de code, nous pouvons définir un cluster Kubernetes pleinement fonctionnel et géré automatiquement par Exoscale.

PHILIPPE CHEPY

Administrateur Système et Développeur