Mon premier cluster Kubernetes

Partage de mes premiers pas dans le monde Kubernetes …

Table of Contents

https://www.linkedin.com/pulse/vers-le-cloud-native-eric-richard

La sécurisation des containers est devenue un sujet incontournable, avec des questions pas triviales tel :

un EDR est-il viable dans cet environnement ?

Il devenait urgent d’acquérir un socle minimum de connaissances.

Une entrée en matière avec l’ouvrage de Yannig PERRE

Mon partage sera en deux parties :

  • les notions de bases
  • les mains dedans !

Rapide prise de conscience :

l’écosystème Kubernetes c’est beaucoup mais alors vraiment beaucoup de notions et concepts

Pourquoi Kubernetes ?

👉 Pour orchestrer ses containers

👉 Pour apporter de la haute disponibilité

👉 Pour gérer la montée en charge

👉 Pour une indépendance du fournisseur : VSphere / AWS / bare metal / …

Par où commencer ?

Deux orientations pour faire ses premiers pas et monter un environnement :

👉 Minikube : la voie “traditionnelle”, un Kubernetes de poche qui permet de travailler localement, un seul nœud qui cumule les rôles de Master et de Worker.

Un all inclusive un peu trop loin de la production pour moi.

👉 Le cluster “standard” : solution retenue.

Le labo

Premier aperçu de l’architecture cible :

👉 Cloisonnement par bulles : appliquons les bonnes pratiques by design !

👉 Certificats LetsEncrypt & gestion via Traefik

👉 DNS récursif avec PIHole qui nous permet de créer des entrées statiques

👉 Virtualisation avec VMWare

👉 Stockage NFS sur un ancien QNAP pour avoir des volumes partagés (le GlusterFS ou CEPH ce sera pour plus tard : step by step)

Un peu de vocabulaire

La logique Kubernetes c’est les poupées russes …

Le vocabulaire qui m’a été indispensable d’acquérir pour comprendre et arriver à mon premier hello world !

  • Nodes

Les serveurs virtuels ou physiques qui portent soit le rôle “Master” soit “nœud d’exécution”.

  • Master

Le responsable du maintien de l’état souhaité du cluster.

Point d’entrée pour l’interaction avec le cluster..

  • Kube API server

Tourne sur le master : expose l’API.

  • Etcd

Tourne sur le master : contient la configuration du cluster.

  • Kube Scheduler

Tourne sur le master : positionne les pods sur le nœud approprié.

  • Kube Controller Manager

Tourne sur le master : surveille le bon fonctionnement du cluster.

  • Worker Nodes

Hôte sur lequel les containers sont exécutés.

  • Kubelet

Tourne sur le node : gère le cycle de vie des containers.

  • Kube Proxy

Tourne sur le node : gère la tuyauterie réseau.

  • Container Runtime

Tourne sur le node : en charge de l’exécution des containers.

  • Pod

C’est une équipe de containers qui se partage des ressources communes..

Les pods sont éphémères.

Le pod est l’unité atomique, un pod est soit en état running soit n’est pas !

  • Service

Les IPs affectées aux pods sont par nature changeantes, les services apportent une couche d’abstraction grâce aux labels.

  • Namespaces

Un cluster virtuel au sein du cluster.

  • Deployments

Descriptif de la politique de déploiement.

Un exemple :

apiVersion: v1
kind: Namespace
metadata:
  name: ctfd
---  
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ctfd
  namespace: ctfd
  labels:
    app: ctfd
spec:
  rules:
  - host: "ctf.homelab.lan"
    http:
      paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: ctfd
              port:
                number: 80
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: ctfd-config
  namespace: ctfd
  labels:
    app: ctfd
data:
  placeholder: "Yes"
  UPLOAD_FOLDER: /var/uploads
  DATABASE_URL: mysql+pymysql://root:ctfd@127.0.0.1:3306/ctfd
  REDIS_URL: redis://127.0.0.1:6379
  WORKERS: "1"
  LOG_FOLDER: /var/log/CTFd
  REVERSE_PROXY: "True"
  MYSQL_ROOT_PASSWORD: ctfd
  MYSQL_USER: ctfd
  MYSQL_PASSWORD: ctfd
  MYSQL_DATABASE: ctfd
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ctfd
  namespace: ctfd
  labels:
    app: ctfd
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
    rollingUpdate:
       maxUnavailable: 25%
       maxSurge: 1
  selector:
    matchLabels:
      app: ctfd
  template:
    metadata:
      labels:
        app: ctfd
    spec:
      containers:
        - name: ctfd
          image: ctfd/ctfd:latest
          ports:
            - containerPort: 8000
          envFrom:
            - configMapRef:
                name: ctfd-config
        - name: db
          image: mariadb:latest
          ports:
            - containerPort: 3306
          envFrom:
            - configMapRef:
                name: ctfd-config
        - name: cache
          image: redis:latest
          ports:
            - containerPort: 6379
          envFrom:
            - configMapRef:
                name: ctfd-config
---
apiVersion: v1
kind: Service
metadata:
  name: ctfd-svc
  namespace: ctfd
spec:
  type: LoadBalancer
  selector:
    app: ctfd
  ports:
  - protocol: TCP
    name: http
    port: 8080
    targetPort: 8000
  • Label

Des paires de clés valeurs qui peuvent être positionnées sur n’importe quel objet Kubernetes.

Un nœud dispose d’un GPU exploitable, un label : “node : gpu” est positionné, on demande aux pods nécessitant cette ressource de s’exécuter exclusivement sur ce nœud.

apiVersion: v1 
kind: Pod 
metadata:
 name: protopod 
 labels: 
  app: myApp 
  env: prod 
spec:
 containers:
  - name: protopod 
    image: docker.io/redteamsfr/proto:v1 
    ports:
    - containerPort: 80 
nodeSelector: 
 gpu: nvidia
  • ConfigMap

Objet dédié au stockage de configuration dans un format clé & valeur.

  • Volume

Le lieu d’échange de fichiers entre pods, de nature persistante ou non.

  • Ingress

Gestion du trafic externe qui rentre dans le cluster.

Et maintenant un petit exercice de synthèse :

Déploiement de Kubernetes

Rentrons dans la salle des machines !

Objectif : déployer mon cluster Kubernetes

Dans un premier temps, le cluster sera déployé sur Debian (instancié avec Packer) , ensuite je migrerai sous CoreOS 4.

Un mot sur CoreOS

C’est un système d’exploitation dédié aux conteneurs. Il est distribué sous la forme d’une image disque qu’il faut “flasher” sur le disque de la machine cible. Ainsi, on flashe le serveur avec l’image CoreOS.

Avantage : on s’assure que tous les serveurs s’appuient sur la même souche logicielle, au bit prêt !

Lors de son premier démarrage, CoreOS va se configurer en suivant les instructions d’un fichier ignition.

Le story board du déploiement

Utilisation de l’environnement infra as code pour éviter de passer du temps sur l’installation de l’OS et sa personnalisation.

Vive Packer !

{
"variables": {
    "timestamp_iso": "2022-05-24 21:27:39",
    "vm-cpu-num": "2",
    "vm-core-num": "2",
    "vm-disk-size": "1000960",
    "vm-mem-size": "2048",
    "vm-name": "LAB-kube-worker01",
    "builder": "vsphere-iso",
    "vm-hostname": "kube-worker01",
    "vm-domain": "homelab.lan",
    "vm-user-username": "xxxxxxx",
    "vm-user-password": "xxxxxxx",
    "iso_url": "ISOS/debian-11.3.0-amd64-netinst.iso",
    "iso_file": "ISOS/debian-11.3.0-amd64-netinst.iso",
    "iso_checksum": "md5:e7a5a4fc5804ae65f7487e68422368ad",
    "http_server_ip": "192.168.1.189",
    "http_server_root": "OUTPUT/2022-05-24_22-27-39_debian10_debian11kube-slave/http_server",
    "headless": "True",
    "vm_description": "Kube Slave",
    "vm_version": "1.0",
    "output_directory": "/vsphere-iso",
    "output_directory_box": "/box",
    "vsphere-user": "xxx@xxx.lan",
    "vsphere-cluster": "Cluster",
[…]
},
"builders": [
    {
        "create_snapshot": "{{user `create_snapshot`}}",
        "content_library_destination": {
            "library": "Templates",
            "name": "{{user `vm-name`}}_tpl",
            "ovf": true
        },
        "type": "vsphere-iso",
        "username": "{{user `vsphere-user`}}",
        "vcenter_server": "{{user `vsphere-server`}}",
        "password": "{{user `vsphere-password`}}",
        "insecure_connection": "true",
        "vm_name": "{{user `vm-name`}}",
        "guest_os_type": "debian10_64Guest",
        "shutdown_command": "echo '{{ user `ssh_password` }}'|sudo -S shutdown -P now",
        "boot_command": [
            "<esc><wait>",
            "auto ",
            "hostname={{user `vm-hostname`}} ",
            "preseed/url=http://{{user `http_server_ip`}}:{{ .HTTPPort }}/preseed.cfg<wait>",
            "<enter>"
        ],
        "boot_order": "disk,cdrom,floppy",
        "boot_wait": "10s",
        "CPUs": "{{user `vm-cpu-num`}}",
        "cpu_cores": "{{user `vm-core-num`}}",
        "RAM": "{{user `vm-mem-size`}}",
        "RAM_reserve_all": false,
        "datacenter": "{{user `vsphere-datacenter`}}",
        "cluster": "{{user `vsphere-cluster`}}",
        "host": "{{user `esxi_host`}}",
        "datastore": "{{user `vsphere-datastore`}}",
        "disk_controller_type": "pvscsi",
[…]
        "storage": [
            {
                "disk_size": "{{user `vm-disk-size`}}",
                "disk_thin_provisioned": true
            }
        ],
        "network_adapters": [
            {
                "network": "DMZ2",
                "network_card": "vmxnet3"
            }
        ],
        "iso_checksum": "{{user `iso_checksum`}}",
        "iso_urls": [
            "{{user `iso_url`}}"
        ]
    }
],
"sensitive-variables": [
    "vsphere-password"
],
"provisioners": [
    {
        "type": "shell",
        "execute_command": "sudo -i bash '{{.Path}}'",
        "scripts": [
            "debian10/provisioner-scripts/update.sh",
            "debian10/provisioner-scripts/sshd-genkeys.sh",
            "debian10/provisioner-scripts/kubernetes.sh"
        ]
    },
    {
        "type": "ansible",
        "playbook_file": "shared/ansible/playbook_linux.yml",
        "ansible_env_vars": [
            "ANSIBLE_HOST_KEY_CHECKING=False",
            "ANSIBLE_REMOTE_TMP = /tmp/.ansible-${USER}/tmp"
        ],
        "extra_arguments": [
            "--extra-vars",
            "ansible_python_interpreter=/usr/bin/python3",
            "--extra-vars",
            "@OUTPUT/2022-05-24_21-27-39_debian10_kube-worker01/generated/ansible_remote_customize_linux.j2.json"
        ],
        "galaxy_file": "shared/ansible/requirements_linux_customize.yml",
        "galaxy_force_install": "true",
        "user": "{{user `vm-user-username`}}"
    },
    {
        "type": "shell",
        "execute_command": "sudo -i bash '{{.Path}}'",
        "scripts": [
            "debian10/provisioner-scripts/sysprep-machine-id.sh",
            "shared/scripts/linux/cleanup.sh"
        ]
    }
],
"post-processors": [
    []
]
}

Préparation du système

(intégré au script de provisionnement)

  • Couche réseau
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
br_netfilter
EOF

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF

sudo sysctl --system
  • Désactivation du swap
sudo swapoff -a 
sudo sed -i '/ swap / s/^/#/' /etc/fstab

Installation du Container Runtime (CRI)

Rappel :

👉 en charge de récupérer les images et de gérer l’exécution des conteneurs

👉 les ordres de mission sont envoyés par Kubelet

OS=Debian_11
CRIO_VERSION=1.23

echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /"|sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
echo "deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$CRIO_VERSION/$OS/ /"|sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$CRIO_VERSION.list

curl -L https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable:cri-o:$CRIO_VERSION/$OS/Release.key | sudo apt-key add -
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo apt-key add -

sudo apt update
sudo apt upgrade -y

sudo apt install -y cri-o cri-o-runc cri-tools
sudo systemctl start crio.service
sudo systemctl enable crio.service

sudo rm -rf /etc/cni/net.d/*

Vérification que cri-o est installé correctement :

systemctl status crio
sudo crictl info
sudo crictl pull hello-world

Par la suite on pourra consulter via crictl les containers actifs :

sudo crictl ps

CONTAINER  IMAGE     CREATED         STATE    NAME                 ATTEMPT  POD ID
99a…       a4c…      40 minutes ago  Running  coredns                  0    bf9…
b14…       a4c…      40 minutes ago  Running  coredns                  0    611…
01a…       …/calico… 40 minutes ago  Running  calico-node              0    428…
eb43…       77b…     41 minutes ago  Running  kube-proxy               0    5a5…
78c…       887…      41 minutes ago  Running  kube-controller-manager  0    3c4…
d75e…       e3e…     41 minutes ago  Running  kube-scheduler           0    bdc…
eb4…       529…      41 minutes ago  Running  kube-apiserver           0    77c…
638…       aeb…      41 minutes ago  Running  etcd                     0    c6c…

Installation de Kubernetes

sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl
sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt update
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

Initialisation du master

sudo kubeadm init --node-name $HOSTNAME --pod-network-cidr=10.244.0.0/16

Par défaut, le cluster ne déploie pas de pods sur le master pour des raisons de sécurité.

Cependant pour faire des économies de ressources, on va s’accorder un petit écart :

kubectl taint nodes --all node-role.kubernetes.io/master-
node/kube-master01 untainted

Après tout ceci on constate que les pods DNS ne démarrent pas … Normal il manque un pod réseau !

kubectl get pods -A

NAMESPACE     NAME                       READY   STATUS    RESTARTS   AGE
kube-system   coredns-6d4b75cb6d-nmqn9   0/1     Pending   0          65m
kube-system   coredns-6d4b75cb6d-w2ksf   0/1     Pending   0          65m
kube-system   etcd-kube-master-node      1/1     Running   1          65m
[…]

Déploiement du pod réseau

kubectl apply -f https://docs.projectcalico.org/v3.23/manifests/calico.yaml

Les pods DNS rentrent en statut running

$ kubectl get pods -A

NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-6d4b75cb6d-nmqn9 1/1 Running 0 68m kube-system coredns-6d4b75cb6d-w2ksf 1/1 Running 0 68m kube-system etcd-kube-master-node 1/1 Running 1 68m […]

Connexion du worker au master

Sur le même modèle on déploie les workers et on les invite à joindre le cluster via le token obtenu précédemment.

sudo kubeadm join 172.16.1.1:6443 --token v1ve5d.7ujz1kdf428v2f4n \
        --discovery-token-ca-cert-hash sha256:858fab067fd697667776b2071ccad4aa56be2d22b66402aa3bb97f5a9b4bb224

Pour voir les tokens actifs :

$ kubeadm token list

TOKEN  TTL    EXPIRES       USAGES                   DESCRIPTION                                                
uu5…   23h    2022-05-27…   authentication,signing   <none>                                                     
v1v…   23h    2022-05-27…   authentication,signing   The …   

Vérification du cluster

Contrôlons l’état de notre cluster :

$ kubectl get nodes

NAME            STATUS     ROLES           AGE   VERSION
kube-master01   Ready      control-plane   45m   v1.24.1
kube-worker01   Ready      <none>          10m   v1.24.1
kube-worker02   Ready      <none>          25s   v1.24.1
kube-worker03   Ready      <none>          6s    v1.24.1

Administration de la plateforme

kubectl est l’outil en ligne de commande pour gérer les ressources du cluster, créer des pods, déployer les manifests de deployment, …

Création du fichier de configuration pour administrer depuis un compte non root.

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

En pratique :

  • Consulter les informations du cluster
kubectl get node -o wide

NAME          STATUS     ROLES           AGE     VERSION  EXTERNAL-IP   OS-IMAGE                        
…master…     Ready      control-plane   18h     v1.24.0   192.168.3.229 Debian…  
…worker03…   NotReady   <none>          3h35m   v1.24.0   192.168.3.124 Debian…   
  • Lister les pods
kubectl get pods -o wide

NAME            READY   STATUS  RESTARTS AGE     IP               NODE                 
nginx-…-6whr4   1/1     Running 2        4h      10.244.214.86    …-worker02-…
nginx-…-crrmm   1/1     Running 2        3h59m   10.244.214.91    …-worker02-…

Des alternatives comme LENS offre une interface graphique sympathique :

Mais commençons à la main 😝

Déploiement de l’administration à distance

# install kubectl

curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
sudo add-apt-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main" 
sudo apt-get install kubectl
# create config directory
mkdir ~/.kube
# copy token
ssh kube-master01 "sudo cat /etc/kubernetes/admin.conf" >~/.kube/config
chmod 600 ~/.kube/config

Une petite touche d’ergonomie pour avoir l’autocomplétion.

sudo apt install -y bash-completion
echo "source <(kubectl completion bash)" >> ~/.bashrc
# ZSH version 
echo "source <(kubectl completion zsh)" >> ~/.zshrc
source <(kubectl completion zsh)

Une coloration syntaxique de kubectl

go get -u github.com/hidetatz/kubecolor/cmd/kubecolor
alias kubectl="kubecolor"

Et on se met confort :

👉 Création des entrées DNS

👉 Création des profils ssh dans le .sshconfig

Host kube-master01
    hostname 172.16.1.1
    User erichard
    IdentityFile ~/.ssh/kubelab-key
    ForwardAgent no
    AddKeysToAgent yes
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null

Host kube-worker01
    hostname 172.16.1.11
    User erichard
    IdentityFile ~/.ssh/kubelab-key
    ForwardAgent no
    AddKeysToAgent yes
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null

Et voilà !

Eric
Eric
🛡️ Cybersecurity enthusiast driven by curiosity and the desire to share.

Related