Skip to content

Kubernetes de zéro

Dans cet article, nous allons monter un cluster Kubernetes pas à pas en installant et configurant chacun des composants, sans passer par un cloud-provider all-in-one (comme GKE ou AWS) ou par un script d’installation (kube-aws, kops ou autre). Cela permet entre autres de se plonger dans le fonctionnement de chaque ressource de Kubernetes pour mieux comprendre ses mécanismes.

1. Préparation de l’environnement

Nous allons monter un cluster qu’on pourrait considérer comme prod-ready et pour cela il se doit entre autres d’être hautement disponible. C’est-à-dire que le fonctionnement de notre cluster devra être tolérant à l’arrêt de certaines machines. Nous utiliserons également TLS pour chiffrer tous les échanges intercluster. J’utiliserai dans ce tutoriel différents termes propres à Kubernetes que je ne détaillerai pas forcément, vous trouverez facilement de quoi il s’agit dans la doc Kubernetes (ou simplement Google).

Dans ce tutoriel, nous allons créer :

  • 3 machines de type ETCD qui formeront un quorum qui contiendra toutes les données et les composantes liées à Kubernetes.
  • 3 machines de types Master qui hébergeront entre autres l’API server de Kubernetes.
  • 3 machines de types Worker où seront installé Docker et où tourneront vos containers.

Pour ensuite ajouter plus de ressources à notre cluster, il suffira d’ajouter au besoin des machines (virtuelles ou physiques) de types Worker.

Dans mon exemple, j’utilise des machines virtuelles, car c’est relativement facile à mettre en place sous n’importe qu’elle distribution (de votre datacenter sur VMWare ou sur votre laptop Windows avec VirtualBox). Vous pouvez bien sûr déployer la même configuration sur des serveurs physiques pour former un cluster dit Bare-Metal. L’OS utilisé sera Ubuntu 16.04 LTS.

Nous allons donc créer 9 machines virtuelles avec 2 Go de RAM chacune et vous pouvez augmenter jusqu’à 8 Go ou 16 Go pour vos Workers qui hébergeront vos containers applicatifs. Cela fait beaucoup de ressources, donc si vous souhaitez monter un cluster de test, vous pouvez mettre ETCD et le Master sur les mêmes machines (ce qui réduit à 6 VMs) ou même utiliser qu’une seule VM Master/ETCD, ce qui enlèverait la haute disponibilité, mais permettrait de monter un cluster sur 1 ou 2 VMs).
Le mieux étant quand même de séparer les composantes.

Récapitulatif de nos 9 machines :

  • etcd0.example.com – 10.240.0.10/24
  • etcd1.example.com – 10.240.0.11/24
  • etcd2.example.com – 10.240.0.12/24
  • master0.example.com – 10.240.0.20/24
  • master1.example.com – 10.240.0.21/24
  • master2.example.com – 10.240.0.22/24
  • worker0.example.com – 10.240.0.30/24
  • worker1.example.com – 10.240.0.31/24
  • worker2.example.com – 10.240.0.32/24

Pour la partie réseau des VMs, nos machines virtuelles sont présentes dans le même réseau 10.240.0.0/24 avec une passerelle en 10.240.0.1. Ce découpage permet d’avoir jusqu’à 253 VMs dans notre environnement ce qui permet d’ajouter ultérieurement pas mal de Workers et de potentielles VMs autour du cluster (nous verrons cela plus tard).

Je vous laisse donc le plaisir de créer vos machines dans votre Hyperviseur favori et les configurer a minima (mis à jour, sécurité…). Pour ma part j’utilise Xen pour les machines virtuelles et Saltstack pour y déployer leurs configurations. D’ailleurs j’ai écrit les recettes pour installer Kubernetes avec Salt si ça peut intéresser quelqu’un.

Génération des certificats

Afin d’assurer le chiffrement des données qui transitent entre nos machines, nous allons mettre en place TLS entre chaque élément du cluster.
Pour générer et signer correctement nos certificats, nous allons utiliser un outil de Cloudflare CfSSL, qui permet de gérer une autorité de certification interne, mais vous pouvez utiliser également OpenSSL comme le décrit la documentation de CoreOS.

Pour les communications dans le cluster, j’utiliserai le hostname des machines plutôt que leurs IPs pour faciliter une renumérotation de réseau éventuelle future.

Pour cela, 3 options s’offrent à vous :

  • Ajouter la correspondance nom/IP dans votre DNS interne
  • Remplacer le hostname par l’IP correspondant dans les configurations qui vont suivre
  • Utiliser un fichier /etc/hosts distribué entre vos VMs (mon option).

Pour commencer, on récupère l’outil CfSSL :

wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64

chmod +x cfssl_linux-amd64

sudo mv cfssl_linux-amd64 /usr/local/bin/cfssl

wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64

chmod +x cfssljson_linux-amd64

sudo mv cfssljson_linux-amd64 /usr/local/bin/cfssljson

On se place ensuite dans un dossier qu’on nommera certs et on génère et signe nos certificats après avoir créé une autorité de certification (CA) :

mkdir /root/certs ; cd /root/certs

echo '{
  "signing": {
    "default": {
      "expiry": "8760h"
    },
    "profiles": {
      "kubernetes": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h"
      }
    }
  }
}' > ca-config.json

echo '{
  "CN": "Kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "NC",
      "L": "Noumea",
      "O": "Kubernetes",
      "OU": "Cluster",
      "ST": "Province-Sud"
    }
  ]
}' > ca-csr.json


cfssl gencert -initca ca-csr.json | cfssljson -bare ca


cat > kubernetes-csr.json <<EOF
{
  "CN": "kubernetes",
  "hosts": [
    "worker0.example.com",
    "worker1.example.com",
    "worker2.example.com",
    "etcd0.example.com",
    "etcd1.example.com",
    "etcd2.example.com",
    "master0.example.com",
    "master1.example.com",
    "master2.example.com",
    "10.32.0.1",
    "127.0.0.1"
  ],
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "NC",
      "L": "Noumea",
      "O": "Kubernetes",
      "OU": "Cluster",
      "ST": "Province-Sud"
    }
  ]
}
EOF

cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kubernetes-csr.json | cfssljson -bare kubernetes

KUBERNETES_HOSTS=(etcd0 etcd1 etcd2 worker0 worker1 worker2 manager0 manager1 manager2)

for host in ${KUBERNETES_HOSTS[*]}; do
  scp ca.pem  ${host}:~/
  scp kubernetes-key.pem  ${host}:~/
  scp kubernetes.pem ${host}:~/
done

N’oubliez pas d’adapter ces commandes avec les données propres à votre cluster (hostnames ou IPs, nombres de machines…). Le fichier kubernetes-csr.json se doit de connaître toutes les hostnames ou IPs autorisés à utiliser ces certificats. La dernière commande copie en SSH tous vos certificats vers les machines qui les requièrent.

Vous devriez avoir les fichiers suivants si tout s’est bien passé :

- ca-key.pem
- ca.pem
- kubernetes-key.pem
- kubernetes.pem

Maintenant que nos certificats sont correctement générés et déployés sur nos machines, on va pouvoir passer à l’installation des composants de Kubernetes.

Dans l’optique d’une mise en production, il faudra bien sûr générer un certificat propre à chaque élément du cluster et non un pour tout le cluster. Il faudra également prévoir un renouvellement de l’autorité de certification, car celle-ci à une durée d’un an.

Mis en place d’ETCD

ETCD est une base de données clé-valeur distribuée créée par CoreOS, un élément essentiel à Kubernetes puisqu’il y stocke toute les informations de votre cluster, comme quel container tourne sur quel Worker et quel service porte quelle IP.

S’il faut donc qu’un service soit hautement disponible et sauvegardé, c’est bien lui.

Nous allons donc installer et configurer ETCD en cluster. Ces commandes sont donc à réaliser sur nos 3 VMs ETCD :

sudo mkdir -p /etc/etcd/

sudo cp ca.pem kubernetes-key.pem kubernetes.pem /etc/etcd/

wget https://github.com/coreos/etcd/releases/download/v3.0.15/etcd-v3.0.15-linux-amd64.tar.gz

tar -xvf etcd-v3.0.15-linux-amd64.tar.gz

sudo mv etcd-v3.0.15-linux-amd64/etcd* /usr/bin/

sudo mkdir -p /var/lib/etcd

On peut désormais créer le fichier unit systemD qui lancera ETCD avec les bons arguments :

cat > etcd.service <<"EOF"
[Unit]
Description=etcd
Documentation=https://github.com/coreos

[Service]
ExecStart=/usr/bin/etcd --name ETCD_NAME \
  --cert-file=/etc/etcd/kubernetes.pem \
  --key-file=/etc/etcd/kubernetes-key.pem \
  --peer-cert-file=/etc/etcd/kubernetes.pem \
  --peer-key-file=/etc/etcd/kubernetes-key.pem \
  --trusted-ca-file=/etc/etcd/ca.pem \
  --peer-trusted-ca-file=/etc/etcd/ca.pem \
  --initial-advertise-peer-urls https://INTERNAL_IP:2380 \
  --listen-peer-urls https://INTERNAL_IP:2380 \
  --listen-client-urls https://INTERNAL_IP:2379,http://127.0.0.1:2379 \
  --advertise-client-urls https://INTERNAL_IP:2379 \
  --initial-cluster-token etcd-cluster-0 \
  --initial-cluster etcd0=https://etcd0.example.com:2380,etcd1=https://etcd1.example.com:2380,etcd2=https://etcd2.example.com:2380 \
  --initial-cluster-state new \
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo mv etcd.service /etc/systemd/system/

Veillez à remplacer INTERNAL_IP par l’IP de votre machine ETCD.

On peut ensuite lancer notre service ETCD :

sudo systemctl daemon-reload
sudo systemctl enable etcd
sudo systemctl start etcd

Une fois cela réalisé sur nos 3 VMs, on peut vérifier la santé de notre cluster comme ceci :

etcdctl --ca-file=/etc/etcd/ca.pem cluster-health

Maintenant que notre ETCD est répliqué, chiffré et en bonne santé, on va pouvoir monter notre Kubernetes sur cette base.

Mis en place des Masters

Un Master Kubernetes comporte 3 composants différents :

  • L’API Server fournit une API REST structurée pour interroger et envoyer des informations dans notre cluster ETCD.
  • Le Scheduler permet de répartir les pods sur les Workers du cluster.
  • Le Controller Manager s’assure que les ressources désirées tournent bien dans notre cluster.

Je vous invite à lire la documentation pour comprendre de manière plus détaillée les fonctions et possibilités qu’offre chaque élément.

D’un point de vue réseau, Kubernetes offre plusieurs possibilités. Dans mon exemple nous allons donc utiliser le plugin CNI de Flannel permettant de définir dynamiquement un range d’IP par Worker pour nos containers qui passeront par un bridge nommé cbr0. Nous allons lui donner dans la configuration le range 10.200.0.0/16 et il découpera un /24 pour chaque Worker (exemple : worker1 = 10.200.1.0/24).

Nos services (manière d’exposer un ou plusieurs containers derrière un couple IP/hostname) doivent également avoir un range d’IP propre à eux, nous utiliserons 10.32.0.0/16 permettant d’avoir jusqu’à 65 536 IPs de services. L’accès aux IPs de services est géré de manière dynamique sur les Workers grâce à des règles IPTables piloté par Kubernetes (kube-proxy).

Nous allons donc récupérer ces éléments dans leur dernière version (v1.4.7) et les configurer correctement avec des fichiers d’units systemD.

Vous pouvez bien sûr remplacer le numéro de version dans les liens par celle que vous souhaitez installer, car Kubernetes est relativement jeune et les nouvelles versions sortent rapidement.

Voici les dernières releases : https://github.com/kubernetes/kubernetes/releases/

Vous devez réaliser ces étapes sur chacune des VMs Masters :

sudo mkdir -p /var/lib/kubernetes

sudo cp ca.pem kubernetes-key.pem kubernetes.pem /var/lib/kubernetes/

wget https://storage.googleapis.com/kubernetes-release/release/v1.4.7/bin/linux/amd64/kube-apiserver
wget https://storage.googleapis.com/kubernetes-release/release/v1.4.7/bin/linux/amd64/kube-controller-manager
wget https://storage.googleapis.com/kubernetes-release/release/v1.4.7/bin/linux/amd64/kube-scheduler
wget https://storage.googleapis.com/kubernetes-release/release/v1.4.7/bin/linux/amd64/kubectl

chmod +x kube-apiserver kube-controller-manager kube-scheduler kubectl

sudo mv kube-apiserver kube-controller-manager kube-scheduler kubectl /usr/bin/

cat > kube-apiserver.service <<"EOF"
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/GoogleCloudPlatform/kubernetes

[Service]
ExecStart=/usr/bin/kube-apiserver \
  --admission-control=NamespaceLifecycle,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota \
  --advertise-address=INTERNAL_IP \
  --allow-privileged=true \
  --apiserver-count=3 \
  --authorization-mode=ABAC \
  --authorization-policy-file=/var/lib/kubernetes/authorization-policy.jsonl \
  --bind-address=0.0.0.0 \
  --enable-swagger-ui=true \
  --etcd-cafile=/var/lib/kubernetes/ca.pem \
  --insecure-bind-address=0.0.0.0 \
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \
  --etcd-servers=https://etcd0.example.com:2379,https://etcd1.example.com:2379,https://etcd2.example.com:2379 \
  --service-account-key-file=/var/lib/kubernetes/kubernetes-key.pem \
  --service-cluster-ip-range=10.32.0.0/16 \
  --service-node-port-range=30000-32767 \
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \
  --token-auth-file=/var/lib/kubernetes/token.csv \
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo mv kube-apiserver.service /etc/systemd/system/

sudo systemctl daemon-reload

sudo systemctl enable kube-apiserver


cat > kube-controller-manager.service <<"EOF"
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/GoogleCloudPlatform/kubernetes

[Service]
ExecStart=/usr/bin/kube-controller-manager \
  --allocate-node-cidrs=true \
  --cluster-cidr=10.200.0.0/16 \
  --cluster-name=kubernetes \
  --leader-elect=true \
  --master=http://INTERNAL_IP:8080 \
  --root-ca-file=/var/lib/kubernetes/ca.pem \
  --service-account-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \
  --service-cluster-ip-range=10.32.0.0/16 \
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF


sudo mv kube-controller-manager.service /etc/systemd/system/

sudo systemctl daemon-reload

sudo systemctl enable kube-controller-manager

cat > kube-scheduler.service <<"EOF"
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/GoogleCloudPlatform/kubernetes

[Service]
ExecStart=/usr/bin/kube-scheduler \
  --leader-elect=true \
  --master=http://INTERNAL_IP:8080 \
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo mv kube-scheduler.service /etc/systemd/system/

sudo systemctl daemon-reload

sudo systemctl enable kube-scheduler

Remplacez bien les occurrences de INTERNAL_IP par l’adresse IP de la machine correspondante.

Pour accéder à notre API Server, cela se fera en HTTPS sur le port 6443 et en HTTP en local uniquement sur le port 8080. Afin de s’identifier auprès de l’API, Kubernetes utilise un système de tokens.

On créé donc le fichier /var/lib/kubernetes/token.csv sur chacune de nos machines contenant des tokens aléatoires pour chacun de nos utilisateurs :

prA5ahie3ohtoo4boogvre6quipheaTh,admin,admin
FWiephivof8onershoong1thee8tosoj,valentin,valentin
hbgith7aWi1aegheifeRhdaiLahVie3z,kubelet,kubelet

On va ensuite créer le fichier /var/lib/kubernetes/authorization-policy.jsonl également sur tous nos Masters :

{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"*", "nonResourcePath": "*", "readonly": true}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"admin", "namespace": "*", "resource": "*", "apiGroup": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"valentin", "namespace": "*", "resource": "*", "apiGroup": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"scheduler", "namespace": "*", "resource": "*", "apiGroup": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user":"kubelet", "namespace": "*", "resource": "*", "apiGroup": "*"}}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"group":"system:serviceaccounts", "namespace": "*", "resource": "*", "apiGroup": "*", "nonResourcePath": "*"}}

N’oubliez pas d’adapter le nom de vos users dans ce fichier. Celui-ci permet de définir les autorisations de chaque utilisateur dans notre cluster.

On va pouvoir enfin démarrer notre Cluster Kubernetes :

sudo systemctl start kube-apiserver

sudo systemctl start kube-controller-manager

sudo systemctl start kube-scheduler

Pour vérifier que tout va bien dans notre cluster, on peut utiliser l’outil kubectl téléchargé précédemment :

kubectl get componentstatuses

NAME                 STATUS    MESSAGE              ERROR
controller-manager   Healthy   ok                   
scheduler            Healthy   ok                   
etcd-1               Healthy   {"health": "true"}   
etcd-0               Healthy   {"health": "true"}   
etcd-2               Healthy   {"health": "true"} 

Vous avez désormais une installation dite High-available de Kubernetes. Cependant, nous n’avons pour l’instant aucun serveur Docker branché à notre cluster, donc il ne sert pas à grand-chose.

Mis en place des Workers

Un Worker Kubernetes se compose de trois éléments, un gestionnaire de containers comme Docker et Rkt (de coreOS) qui s’occupe de lancer et gérer vos containers, Kubelet qui est un élément propre à Kubernetes permettant de piloter vos containers, vos images, vos volumes (…) et Kube-Proxy qui s’occupe de gérer l’iptables de votre Worker afin de router les IPs de services vers les bons pods.

Dans ce post, on va se consacrer à Docker, mais promis je reparlerais bientôt de Rkt.

On va donc récupérer Docker en version 1.12.3, Kubelet et Kube-proxy sur chacun de nos Workers et les configurer par la suite :

sudo mkdir -p /var/lib/kubernetes

sudo cp ca.pem kubernetes-key.pem kubernetes.pem /var/lib/kubernetes/

wget https://get.docker.com/builds/Linux/x86_64/docker-1.12.3.tgz

tar -xvf docker-1.12.3.tgz

sudo cp docker/docker* /usr/bin/

sudo sh -c 'echo "[Unit]
Description=Docker Application Container Engine
Documentation=http://docs.docker.io

[Service]
ExecStart=/usr/bin/docker daemon \
  --iptables=false \
  --ip-masq=false \
  --host=unix:///var/run/docker.sock \
  --log-level=error \
  --storage-driver=overlay
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target" > /etc/systemd/system/docker.service'
sudo systemctl daemon-reload
sudo systemctl enable docker
sudo systemctl start docker
sudo docker version


sudo mkdir -p /opt/cni

wget https://storage.googleapis.com/kubernetes-release/network-plugins/cni-07a8a28637e97b22eb8dfe710eeae1344f69d16e.tar.gz

sudo tar -xvf cni-07a8a28637e97b22eb8dfe710eeae1344f69d16e.tar.gz -C /opt/cni

wget https://storage.googleapis.com/kubernetes-release/release/v1.4.7/bin/linux/amd64/kube-proxy
wget https://storage.googleapis.com/kubernetes-release/release/v1.4.7/bin/linux/amd64/kubelet

chmod +x kubectl kube-proxy kubelet

sudo mv kubectl kube-proxy kubelet /usr/bin/

sudo mkdir -p /var/lib/kubelet/

sudo sh -c 'echo "apiVersion: v1
kind: Config
clusters:
- cluster:
    certificate-authority: /var/lib/kubernetes/ca.pem
    server: https://master0.example.com:6443
  name: kubernetes
contexts:
- context:
    cluster: kubernetes
    user: kubelet
  name: kubelet
current-context: kubelet
users:
- name: kubelet
  user:
    token: hbgith7aWi1aegheifeRhdaiLahVie3z" > /var/lib/kubelet/kubeconfig'

sudo sh -c 'echo "[Unit]
Description=Kubernetes Kubelet
Documentation=https://github.com/GoogleCloudPlatform/kubernetes
After=docker.service
Requires=docker.service

[Service]
ExecStart=/usr/bin/kubelet \
  --allow-privileged=true \
  --api-servers=https://master0.example.com:6443,https://master1.example.com:6443,https://master2.example.com:6443 \
  --cloud-provider= \
  --cluster-dns=10.32.0.10 \
  --cluster-domain=cluster.local \
  --configure-cbr0=true \
  --container-runtime=docker \
  --docker=unix:///var/run/docker.sock \
  --network-plugin=kubenet \
  --kubeconfig=/var/lib/kubelet/kubeconfig \
  --reconcile-cidr=true \
  --serialize-image-pulls=false \
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \
  --v=2

Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target" > /etc/systemd/system/kubelet.service'

sudo systemctl daemon-reload

sudo systemctl enable kubelet

sudo systemctl start kubelet

sudo sh -c 'echo "[Unit]
Description=Kubernetes Kube Proxy
Documentation=https://github.com/GoogleCloudPlatform/kubernetes

[Service]
ExecStart=/usr/bin/kube-proxy \
  --master=https://master0.example.com:6443 \
  --kubeconfig=/var/lib/kubelet/kubeconfig \
  --proxy-mode=iptables \
  --v=2

Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target" > /etc/systemd/system/kube-proxy.service'

sudo systemctl daemon-reload

sudo systemctl enable kube-proxy

sudo systemctl start kube-proxy    

Comme nous avons défini un range d’IPs en /24 par Worker, il faut que nos containers puissent passer d’un Worker à un autre afin de se parler entre eux (exemple : une application et sa base de donnée peuvent être sur deux Workers différents). Pour cela, il faut que nous Workers ait une route vers chacun de ses voisins.

Vous pouvez gérer cela de manière dynamique avec un gestionnaire de configuration (comme Salt ou Ansible), ou simplement ajouter les routes à la main sur chacun des Workers :

ip route add 10.200.0.0/24 via 10.240.0.30

ip route add 10.200.1.0/24 via 10.240.0.31

ip route add 10.200.2.0/24 via 10.240.0.32

Il faudra cependant ne pas oublier d’ajouter une route à chaque fois qu’on ajoute un Worker au cluster.

Nos Workers sont désormais prêts et reliés à notre cluster Kubernetes, pour vérifier cela, on se connecte à une de nos VMs Masters et on saisit cette commande :

kubectl get nodes

Configuration du client kubectl

Kubectl est l’outil en ligne de commande permettant de piloter notre cluster Kubernetes. Comme les autres composants, il va se connecter de manière chiffrée à notre API Server, il faut donc posséder le certificat client sur la machine ou l’on souhaite configurer kubectl. Il vous faudra également le Token qui correspond à votre utilisateur, qu’on a créé plus haut.

Sur nos VMs Masters, kubectl est installé et n’a pas besoin d’authentification comme ils communiquent en local. Vous pouvez donc effectuer les prochaines étapes depuis un Master plutôt que de configurer un client externe.

On va donc récupérer l’outil Kubectl pour Linux ou pour Mac :

## Mac
wget https://storage.googleapis.com/kubernetes-release/release/v1.4.7/bin/darwin/amd64/kubectl

## Linux
wget https://storage.googleapis.com/kubernetes-release/release/v1.4.7/bin/linux/amd64/kubectl

chmod +x kubectl
sudo mv kubectl /usr/local/bin

kubectl config set-cluster my-prod-cluster \
  --certificate-authority=ca.pem \
  --embed-certs=true \
  --server=https://master0.example.com:6443


kubectl config set-credentials admin --token prA5ahie3ohtoo4boogvre6quipheaTh

kubectl config set-context default-context \
  --cluster=my-prod-cluster \
  --user=admin

kubectl config use-context default-context

kubectl get componentstatuses

kubectl get nodes

NAME      STATUS    AGE
worker0   Ready     7m
worker1   Ready     5m
worker2   Ready     2m

Mis en place du DNS Interne (kube-dns)

Notre cluster est désormais fonctionnel et hautement disponible avec 3 workers Docker prêts à recevoir nos applications et à les scaller à volonté. Cependant, nos pods (un groupe de containers sur une même IP) ont bien accès à Internet, mais n’ont pas de résolution DNS externe et interne.

Pour régler ce problème, nous allons déployer le service SkyDNS afin d’avoir de la résolution en interne dans le cluster sur l’IP de service 10.32.0.10 qui permettra de résoudre dynamiquement les noms de nos services afin de pouvoir utiliser des hostnames plutôt que des IPs. (Par exemple, dans la configuration de la base de données pour site WordPress, vous mettrait « mysql-wordpress » plutôt que l’IP du service qui elle, peut changer).

echo "

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: kube-dns-v20
  namespace: kube-system
  labels:
    k8s-app: kube-dns
    version: v20
    kubernetes.io/cluster-service: "true"
spec:
  replicas: 3
  selector:
    matchLabels:
      k8s-app: kube-dns
      version: v20
  template:
    metadata:
      labels:
        k8s-app: kube-dns
        version: v20
        kubernetes.io/cluster-service: "true"
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ''
        scheduler.alpha.kubernetes.io/tolerations: '[{"key":"CriticalAddonsOnly", "operator":"Exists"}]'
    spec:
      containers:
      - name: kubedns
        image: gcr.io/google_containers/kubedns-amd64:1.8
        resources:
          limits:
            memory: 170Mi
          requests:
            cpu: 100m
            memory: 70Mi
        livenessProbe:
          httpGet:
            path: /healthz-kubedns
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 60
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 5
        readinessProbe:
          httpGet:
            path: /readiness
            port: 8081
            scheme: HTTP
          # we poll on pod startup for the Kubernetes master service and
          # only setup the /readiness HTTP server once that's available.
          initialDelaySeconds: 3
          timeoutSeconds: 5
        args:
        # command = "/kube-dns"
        - --domain=example.com
        - --dns-port=10053
        ports:
        - containerPort: 10053
          name: dns-local
          protocol: UDP
        - containerPort: 10053
          name: dns-tcp-local
          protocol: TCP
      - name: dnsmasq
        image: gcr.io/google_containers/kube-dnsmasq-amd64:1.4
        livenessProbe:
          httpGet:
            path: /healthz-dnsmasq
            port: 8080
            scheme: HTTP
          initialDelaySeconds: 60
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 5
        args:
        - --cache-size=1000
        - --no-resolv
        - --server=127.0.0.1#10053
        - --log-facility=-
        ports:
        - containerPort: 53
          name: dns
          protocol: UDP
        - containerPort: 53
          name: dns-tcp
          protocol: TCP
      - name: healthz
        image: gcr.io/google_containers/exechealthz-amd64:1.2
        resources:
          limits:
            memory: 50Mi
          requests:
            cpu: 10m
            memory: 50Mi
        args:
        - --cmd=nslookup kubernetes.default.svc.example.com 127.0.0.1 >/dev/null
        - --url=/healthz-dnsmasq
        - --cmd=nslookup kubernetes.default.svc.example.com 127.0.0.1:10053 >/dev/null
        - --url=/healthz-kubedns
        - --port=8080
        - --quiet
        ports:
        - containerPort: 8080
          protocol: TCP
      dnsPolicy: Default
---
apiVersion: v1
kind: Service
metadata:
  labels:
    k8s-app: kube-dns
    kubernetes.io/cluster-service: "true"
    kubernetes.io/name: KubeDNS
  name: kube-dns
  namespace: kube-system
  resourceVersion: "223165"
spec:
  clusterIP: 10.32.0.10
  ports:
  - name: dns
    port: 53
    protocol: UDP
    targetPort: 53
  - name: dns-tcp
    port: 53
    protocol: TCP
    targetPort: 53
  selector:
    k8s-app: kube-dns
  sessionAffinity: None
  type: ClusterIP

" | kubectl create -f -

Ce déploiement de Kube-DNS va créer un pod de 3 containers par Worker afin de permettre une résolution de nom des services propres au cluster et de fournir de la résolution de nom externe pour nos containers.

On vérifie que tout est bien créé :

kubectl get pod --all-namespaces


kube-system     kube-dns-v20-3504276524-6xon3                3/3       Running   0         1d       10.200.0.229   worker0.example.com
kube-system     kube-dns-v20-3504276524-vcmag                3/3       Running   0          1d       10.200.2.122   worker2.example.com
kube-system     kube-dns-v20-3504276524-wzaae                3/3       Running   0          1d       10.200.1.237   worker1.example.com

Pour être sûr que la résolution DNS fonctionne, on va tester avec un petit pod Busybox, bien pratique :

kubectl run -i -t busybox --image=busybox --restart=Never


/ # nslookup google.com
Server:    10.32.0.10
Address 1: 10.32.0.10 kube-dns.kube-system.svc.k8.noc.nc

Name:      google.com
Address 1: 2404:6800:4006:807::200e syd09s13-in-x0e.1e100.net
Address 2: 61.5.222.166

Accès externe aux services

L’accès externe aux services est une problématique assez complexe dans l’univers de Kubernetes, car elle permet plusieurs approches. Dans le cas d’un cluster GKE, le type de services LoadBalancer permet de demander une IP publique pour exposer notre service. C’est pratique, mais nous, on l’a monté nous même notre cluster, donc pas de mécanisme de provisionning d’IP publiques.

Plusieurs choix s’offrent à vous :

  • Utiliser le type NodePort qui expose un service sur un port élevé de tous vos Workers. Cela peut être pratique pour tester le bon fonctionnement de son service, mais plus complexe pour mettre en place un reverse-proxy.
  • Utiliser une IP publique dans le champ ExternalIPs d’un service. Cela est une approche très pratique, mais il faut avoir des IP publiques routées vers nos Worker, donc pas possibles dans un environnement de test.
  • Utiliser un Ingress controller comme Traefik ou Nginx Ingress controller et de les exposer en mode NodePort sur les ports 80/443 de nos Workers. Cela permet de définir des noms d’accès à nos services (Ingress), l’équivalent d’un VirtualHost, et qu’un reverse proxy les servent dynamiquement et de manière chiffrée (TLS). Je détaillerai cette installation dans un futur article.

Smoke tests

Pour vérifier que tout fonctionne, on va se créer un petit déploiement avec 3 replicas pour voir si Kubernetes répartit bien les containers sur tous nos Workers. Nous l’exposerons ensuite en mode NodePort.

kubectl run nginx --image=nginx --port=80 --replicas=3

kubectl expose deployment nginx --type NodePort

NODE_PORT=$(kubectl get svc nginx --output=jsonpath='{range .spec.ports[0]}{.nodePort}')

curl http://worker0.example.com:${NODE_PORT}

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Notre déploiement de Nginx est exposé en mode NodePort sur tous nos Workers et load-balance entre 3 replicas reparties sur nos serveurs Docker.

Voilà, c’est fini pour aujourd’hui. Nous avons donc monté un cluster hautement disponible et qui communique de manière chiffrée. Une petite chose à améliorer cependant, il est préférable selon moi d’utiliser un outil comme Keepalived sur les machines Masters afin d’avoir une IP partagée entre ces 3 machines. Cela permet d’être totalement tolérant à l’extinction de 2 VMs sur 3. Actuellement certains services (kube-proxy ou kubelet) dépendant d’une seule API-Server.

Dans un prochain article, je vous expliquerai comment ajouter des add-ons à notre cluster afin d’avoir entre autres, le dashboard Kubernetes, Grafana pour les métriques et Kibana pour les logs de notre cluster. Nous mettrons en place Nginx Ingress Controller et Kube-lego pour exposer nos services en HTTPS avec Let’s Encrypt. Puis nous verrons comment utiliser les outils Kompose, pour convertir des fichiers Docker-compose en déploiement Kubernetes et Helm qui est en quelque sorte un package manager d’application pour Kubernetes. Je vous ferais également un article pour parler des mécanismes de volumes externes à Kubernetes comme GlusterFS qui permettent d’avoir les données des containers externalisés des Workers.

Je remercie tout particulièrement Kelsey Hightower pour son dépôt « Kubernetes-The-Hard-Way » qui m’a franchement bien aidé et Mikaël Cluseau qui m’a supporté dans mes longues interrogations autour de ce fabuleux tools qu’est Kubernetes.

Published inDevOpsDockerKubernetesLinuxNetworkingTLSTutoriel

23 Comments

  1. bobricard bobricard

    Ça a l’air intuitif ;), non euj déconne ! Jolie l’usine à gaz 😉

  2. Jocelyn Jocelyn

    Belle doc! il manque la config pour le service kubelet? Merci!

  3. valentin valentin

    Ah merci Jocelyn, j’avais oublié une petit partie, c’est modifié. Autant pour moi. 🙂

  4. Arthur Arthur

    J’ai un bug lorsque je tape la commande etcdctl –ca-file=/etc/etcd/ca.pem cluster-health
    , j’ai un message d’erreur disant :

    Error: client: etcd cluster is unavailable or misconfigured
    error #0: dial tcp 127.0.0.1:4001: getsockopt: connection refused
    error #1: dial tcp 127.0.0.1:2379: getsockopt: connection refused

    Une suggestion ? :'(

    • valentin valentin

      Hello,

      Cela signifie qu’ETCD n’est visiblement pas bien lancé,
      Je te conseille de regarder les logs ETCD de cette manière :
      `journactl -u etcd -f`

      • Arthur Arthur

        J’ai réussi à corrigé mon erreur en supprimant « –name ETCD_NAME \ » dans le fichier de conf ! 🙂

  5. Nadir Nadir

    Hello ! Super doc que je vais m’empresser de suivre pour tester !
    Tu possèdes encore les recettes Salt que tu as mentionné dans le tuto 🙂 ?

    • valentin valentin

      Salut,

      contact moi par mail sur valentin [at] ouvrard.it et je te filerai ça.

      🙂

  6. ph ph

    hello, bonne documentation, bien méthodique.
    par contre je n’ai pas pu relier le configuraiton de CNI pour mes workers, aurais-tu des détails à me donner?

    • valentin valentin

      Hello Ph,

      Que veux-tu dire par relier ?

      Dans ce tutoriel j’utilise le mode le plus simple, kubenet, donc normalement rien de particulier à faire (il prend sa conf sur les arguments de kubelet).

      Si tu veux d’autres CNI plugin (calico ou autre) il faudra que les binaires existes sur tes workers.

  7. SF SF

    Salut,
    Merci pour ce tutoriel.
    Je n’arrive pas à ajouter les workers, sur la machine master la commande « kubectl get nodes  » ne renvoie rien.

    • valentin valentin

      Regarde les logs du service Kubelet (journalctl -u kubelet -f ) et tu sauras pourquoi il ne se register pas avec ton API-server ! 🙂
      Regarde aussi si kube-proxy se connecte bien.

      • SF SF

        J’ai créé trois machines (pour le moment ) avec Vagrant :
        master1 :192.168.33.10
        slave1 : 192.168.33.12
        etcd0 :192.168.33.14

        journalctl -u kubelet -f :
        Aug 18 08:38:20 slave1 kubelet[1143]: E0818 08:38:20.207565 1143 kubelet_node_status.go:97] Unable to register node « slave1 » with API server: Post https://192.168.33.10:6443/api/v1/nodes: x509: certificate signed by unknown authority (possibly because of « crypto/rsa: verification error » while trying to verify candidate authority certificate « Kubernetes »)
        journalctl -u kube-proxy -f
        Aug 18 08:41:28 slave1 kube-proxy[1159]: E0818 08:41:28.052113 1159 reflector.go:203] pkg/proxy/config/api.go:33: Failed to list *api.Endpoints: Get https://192.168.33.10:6443/api/v1/endpoints?resourceVersion=0: x509: certificate signed by unknown authority (possibly because of « crypto/rsa: verification error » while trying to verify candidate authority certificate « Kubernetes »)

        PS:
        J’ai changé la route : ip route add 10.200.0.0/24 via 192.168.33.14
        Merci d’avance,

        • valentin valentin

          Hummm ça semble être un problème au niveau des certificats qui sont pas trustés. As tu bien mis tout les noms possible (dont les IPs) dans le json pour créer les certificats de k8s ? Ton kubelet à t’il bien accès au ca.pem et sa conf pointe t’il bien sur lui ?

          Je penses que tu devrais peut être repartir de zéro en régénèrant tes certs avec les bons noms (IP & nom).

          • SF SF

            J’ai renouvelé mes certificats, et il parait que le service kubelet n’est pas démarré :
            systemctl status kubelet
            ● kubelet.service – Kubernetes Kubelet
            Loaded: loaded (/etc/systemd/system/kubelet.service; enabled; vendor preset: enabled)
            Active: activating (auto-restart) (Result: exit-code) since Fri 2017-08-18 11:58:33 UTC; 714ms ago
            Docs: https://github.com/GoogleCloudPlatform/kubernetes
            Process: 14932 ExecStart=/usr/bin/kubelet –allow-privileged=true –api-servers=https://192.168.33.10:6443 –cloud-provider= –cluster-dns=10.32.0.10 –cluster-domain=cluster.local –configure-cbr0=true — Main PID: 14932 (code=exited, status=203/EXEC)

            Aug 18 11:58:33 slave systemd[1]: kubelet.service: Unit entered failed state.
            Aug 18 11:58:33 slave systemd[1]: kubelet.service: Failed with result ‘exit-code’.

            journalctl -u kubelet -f
            Stopped Kubernetes Kubelet.
            Aug 18 11:58:59 slave systemd[1]: Started Kubernetes Kubelet.
            Aug 18 11:58:59 slave systemd[1]: kubelet.service: Main process exited, code=exited, status=203/EXEC
            Aug 18 11:58:59 slave systemd[1]: kubelet.service: Unit entered failed state.
            Aug 18 11:58:59 slave systemd[1]: kubelet.service: Failed with result ‘exit-code’.
            Aug 18 11:59:05 slave systemd[1]: kubelet.service: Service hold-off time over, scheduling restart.
            Aug 18 11:59:05 slave systemd[1]: Stopped Kubernetes Kubelet.
            Aug 18 11:59:05 slave systemd[1]: Started Kubernetes Kubelet.
            Aug 18 11:59:05 slave systemd[1]: kubelet.service: Main process exited, code=exited, status=203/EXEC
            Aug 18 11:59:05 slave systemd[1]: kubelet.service: Unit entered failed state.
            Aug 18 11:59:05 slave systemd[1]: kubelet.service: Failed with result ‘exit-code’.

            J’ai remplacé « worker0.example.com » par 192.168.33.10 mais n’empêche que le dns marche : ping etcd0 : ok!!

  8. SF SF

    C’est un problème de permissions : j’ai ajouté la commande de chmod +x /usr/bin/kubelet
    ça marche maintenant j’arrive à voir le noeud worker .
    Je vais ajouter plus de machines au cluster.
    Merci pour tes réponses ,

  9. Salut Valentin,

    Super article ! J’attends toujours ton prochain article sr la suite de la mise à place de K8s (dashboard, kibana, graphana…)

    A plus !

  10. jahack jahack

    salut,

    Merci pour cet article !!
    Le script de génération des certificats est à exécuter une seule fois ou doit il être exécuté sur chaque VM ?
    Actuellement je génère mes 3 fichiers .pem et je les envoi sur toutes les VM dans le répertoire /root/certs.
    par contre lors de la création du cluster etcd j’ai une erreur :

    failed to check the health of member 68afffba56612fd on https://192.168.1.31:2379: Get https://192.168.1.31:2379/health: x509: certificate signed by unknown authority (possibly because of « crypto/rsa: verification error » while trying to verify candidate authority certificate « Kubernetes »)

    Pourtant pour la génération des certificats, j’ai bien renseignée les différentes IP et hostname.

    • jahack jahack

      problème résolu,
      avant de recharger une configuration pour le service etcd il fallait faire un kill de celui-ci.

      • valentin valentin

        Super !
        Si tu ajoutes des nodes, tu devrais resigner tes certs avec le nouveau node et redéployer ces certs sur les machines de ton cluster.
        Si tu envisages de la production avec Kubernetes, je t’encourage à utiliser un gestionnaire de configuration comme Saltstack pour gérer cela.

  11. Nouali Lotfi Nouali Lotfi

    Merci pour cet article. Très clair.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Proudly hosted with Open-sources Softwares.