Kubernetes : comment éviter une catastrophe involontaire ?

Magazine
Marque
GNU/Linux Magazine
Numéro
259
Mois de parution
septembre 2022
Spécialité(s)


Résumé

Kubernetes, vous trouvez ça génial, mais rien ne vous empêche d’exécuter des commandes qui vont casser votre beau jouet. Alors, comment réduire ces risques ? En implémentant un « Admission Controller » !


Body

Pour gérer les clusters Kubernetes, nous utilisons principalement la commande kubectl. Or, un jour, un collègue a voulu détruire tous les Pods existants en exécutant la commande « kubectl delete po --all ». Seulement, il a fait une coquille et a exécuté la commande « kubectl delete no --all » ! Voyez-vous la différence ? Il a répondu « yes » à la question posée et a donc détruit tous les Nodes au lieu de détruire tous les Pods ! Vent de panique, tout est en rade... Que faire ? Pas le choix, il faut appliquer la procédure de restauration du cluster etcd et espérer qu’elle fonctionne !

Le calme revient et une réflexion s’impose : que pourrait-on faire pour éviter que cette mauvaise expérience ne se reproduise ? L’architecture de Kubernetes nous propose une solution plutôt simple : écrivons un Admission Controller qui va interdire les destructions d’objets jugés sensibles !

1. Kubernetes et les Admission Controllers

1.1 Petit rappel sur l’architecture de Kubernetes

La figure 1 ci-dessous rappelle les étapes qui interviennent lorsqu’on soumet une requête à l’Apiserver de Kubernetes :

apiserver-s

Fig. 1 : Architecture de l’Apiserver.

Après une étape d’authentification, puis de vérification des autorisations, la requête est passée à un ensemble de Controllers dits Admission Controllers dont le rôle est double : soit ils décident de modifier la requête originale et on parle alors de Mutating Admission Controllers, soit ils décident de rejeter la requête et on parle alors de Validating Admission Controllers. Un Admission Controller peut remplir les deux rôles.

Certains Admission Controllers sont compilés dans le code de l’Apiserver (c’est le cas de LimitRanger, ResourceQuota, DefaultStorageClass, etc.). Dans le cas contraire, les Admission Controllers sont des programmes externes qui proposent un service HTTP/S appelé par le MutatingAdmissionWebhook Controller ou le ValidatingAdmissionWebhook Controller. Ces programmes externes sont alors appelés des Admission Webhooks.

1.2 Notre objectif et ses composants

Nous allons donc développer un Validating Admission Webhook qui va rejeter les requêtes de destruction (DELETE) des objets sensibles comme, par exemple, les Nodes, les Secrets ou bien certains Pods. La figure 2 décrit les objets que nous allons devoir définir ainsi que leurs relations.

k8s-objects-s

Fig. 2 : Composants à déployer.

Pour des raisons évidentes de haute disponibilité et de facilité d’exploitation, notre Admission Webhook sera « containerisé » et démarré dans un Pod contrôlé indirectement par un Deployment (nous disons « indirectement », car rappelez-vous qu’un Deployment contrôle un ReplicaSet qui contrôle un ou plusieurs Pod !).

Notre Admission Webhook aura besoin d’être configuré, en particulier il faudra lui donner les spécifications des règles à appliquer pour autoriser ou interdire les demandes de destruction. Il faudra que la configuration puisse être communiquée lors du lancement du Pod et non pas « codée en dur » dans l’image du Container. Plusieurs solutions sont disponibles : nous pourrions faire en sorte que notre Admission Webhook gère ses propres types d’objets, et nous avons déjà écrit un article à ce sujet [2]. Nous allons faire plus simple ici : les règles de configuration seront stockées dans une ConfigMap qui sera montée sous forme de Volume, dans le Container (voir §3.2).

Pour finir, comme notre Webhook communique via HTTPS, il aura besoin d’une clé privée et d’un certificat. Ces informations lui seront transmises sous forme de Secret (voir §3.2).

2. Implémentation de l’Admission Webhook

2.1 Squelette du service web

Comme un Admission Webhook propose avant tout un service HTTP/S qui sera appelé par l’Apiserver, il faut écrire… une application web ! Le langage de prédilection dans le monde des Containers est le langage Go et nous allons l’utiliser ici, mais son usage n’est absolument pas une obligation (d’ailleurs, l’article précité [2] montre une réalisation de Custom Controller en Python).

Notre 1re étape consiste à écrire le corps d’un service web, ce qui donne le code suivant (oui, ce code ne « compile » pas encore et comporte des importations inutiles pour l’instant !) :

# fichier main.go
package main
 
import (
        "context"
        "crypto/tls"
        "flag"
        "fmt"
        log "github.com/sirupsen/logrus"
        "net/http"
        "os"
        "os/signal"
        "syscall"
)
 
func main() {
        port:= 8443
 
        server := &http.Server{
                Addr: fmt.Sprintf(":%v", port),
                TLSConfig: &tls.Config{
                        Certificates: []tls.Certificate{certs},
                },
        }
 
        mux := http.NewServeMux()
        server.Handler = mux
 
        go func() {
                log.Printf("Listening on port %v", port)
                if err := server.ListenAndServeTLS("", ""); err != nil {
                        log.Fatalf("Failed to listen and serve webhook server: %v", err)
                }
        }()
 
        // Listen to the shutdown signal
        signalChan := make(chan os.Signal, 1)
        signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
        <-signalChan
 
        log.Printf("Shutting down webserver")
        server.Shutdown(context.Background())
}

Il nous faut compléter ce squelette de code !

2.2 Génération et intégration des clés et certificats

Comme vous pouvez le lire, le service web doit utiliser HTTPS. Dans ce cas, l’Apiserver va contacter notre Admission Webhook et va vouloir vérifier la validité de son certificat. Il faut donc créer une paire de clés publiques/privées et un certificat signé par une autorité de certification de confiance (nous-mêmes), puis il faut donner à notre Admission Webhook le nom des fichiers contenant la clé privée et le certificat.

Le dépôt du projet [1] contient le script generate-certs.sh qui peut vous servir pour créer une clé privée et un certificat pour une AC ainsi qu’une clé privée et un certificat pour notre Webhook. Les fichiers sont créés dans le sous-répertoire ./certs, comme le montre la commande ci-dessous :

$ tree certs
certs
├── ca.key
├── ca.pem
├── ca.srl
├── manifest.yaml
├── server.crt
└── server-key.pem

Le certificat est créé pour le service k8s-delete-protection.kube-system.svc, mais vous pouvez éditer le script shell pour modifier cette valeur via les variables service et namespace situées au début du script.

Nous ajoutons à notre programme des options pour que les noms des fichiers contenant le certificat et la clé privée puissent être passés en paramètre. On en profite aussi pour paramétrer la valeur du port TCP sur lequel va écouter le service. Le code devient :

func main() {
        var certFile, keyFile, port string
 
        flag.StringVar(&certFile, "cert", "server.pem",
                "File containing the x509 Certificate for HTTPS")
        flag.StringVar(&keyFile, "key", "server-key.pem",
                "File containing the x509 private key for the given certificate")
        flag.StringVar(&port, "port", "8443", "Port to listen")
 
        flag.Parse()
 
        certs, err := tls.LoadX509KeyPair(certFile, keyFile)
        if err != nil {
                log.Fatalf("Error loading key pair: %v", err)
        }
        ...
}

Compilons notre service web et testons-le :

$ CGO_ENABLED go build -o ../../bin
$ ../../bin/main -cert certs/server.crt -key certs/serverkey.pem
INFO[0000] Listening on port 8443

Dans une autre fenêtre, testons l’accès au service web :

$ curl https://localhost:8443
curl : (60) SSL certificate problem : self signed certificate
 
$ curl -k http://localhost:8443
404 page not found

Aucun page n’est disponible, mais au moins, notre Webhook répond !

2.3 Gestion des requêtes

Notre service est prêt à l’emploi, mais il ne supporte aucune requête. Nous devons choisir la valeur de l’URL qui sera appelée par l’Apiserver pour valider les ordres de destruction des objets. Nous appellerons arbitrairement /validate cette URL. Nous devons aussi connecter cette URL à la fonction de traitement de la requête. Le code précédent :

        mux := http.NewServeMux()
        server.Handler = mux

devient :

        mux := http.NewServeMux()
        mux.HandleFunc("/validate", handleAdmissionRequest)
        server.Handler = mux

Le code de la fonction handleAdmissionRequest est contenu dans le fichier server.go. La première chose à faire est de convertir le contenu de la requête HTTP reçue en une structure AdmissionReview :

import (
        "encoding/json"
        "fmt"
        log "github.com/sirupsen/logrus"
        "io/ioutil"
        admission "k8s.io/api/admission/v1"
        k8meta "k8s.io/apimachinery/pkg/apis/meta/v1"
        "net/http"
)
 
func handleAdmissionRequest(w http.ResponseWriter, r *http.Request) {
        var body []byte
        if r.Body != nil {
                data, err := ioutil.ReadAll(r.Body)
                if err == nil {
                        body = data
                } else {
                        log.Infof("Error %v", err)
                        http.Error(w, "Error reading body", http.StatusBadRequest)
                        return
                }
        }
        if len(body) == 0 {
                log.Error("Body is empty")
                http.Error(w, "Body is empty", http.StatusBadRequest)
                return
        }
 
        request := admission.AdmissionReview{}
        if err := json.Unmarshal(body, &request); err != nil {
                log.Errorf("Error parsing body %v", err)
                http.Error(w, "Error parsing body", http.StatusBadRequest)
                return
        }
        ...

La variable request est une structure de type AdmissionReview et son champ Request contient des informations utiles pour notre Admission Webhook [3] :

  • request.Request.Operation indique le type de l’opération demandée (CREATE, UPDATE, DELETE…) ;
  • request.request.Kind.Kind donne le type d’objet concerné (Scale, Pod, Deployment, Node…) ;
  • request.Request.Name est le nom de l’objet impacté par l’opération ;
  • request.Request.Namespace donne le Namespace auquel appartient la ressource impactée.

Nous invoquons maintenant le « cerveau » de notre Webhook, à savoir la fonction checkRequest() qui doit décider du sort de l’opération : autorisée ou interdite ?

La fonction checkRequest() retourne un booléen qui autorise ou non l’opération. Dans le cas d’un refus (false), elle retourne aussi une error contenant un message qui sera affiché sur le terminal de l’utilisateur. Muni de ces informations, nous pouvons construire un objet de type AdmissionReview et le retourner à l’Apiserver.

La structure AdmissionReview contient certains éléments issus de l’objet request ainsi que notre réponse (structure AdmissionResponse) qui contient l’UID de la requête et le résultat de la fonction checkRequest().

Nous pouvons compléter maintenant le code de la fonction handleAdmissionRequest() :

        result, err := checkRequest(request.Request)
        response := admission.AdmissionResponse{
                UID:     request.Request.UID,
                Allowed: result,
        }
        if err != nil {
                response.Result = &k8meta.Status{
                        Message: fmt.Sprintf("%v", err),
                        Reason: k8meta.StatusReasonForbidden,
                }
        }
 
        outReview := admission.AdmissionReview{
                TypeMeta: request.TypeMeta,
                Request: request.Request,
                Response: &response,
        }
        json, err := json.Marshal(outReview)
 
        if err != nil {
                log.Errorf("json.Marshal error %v", err)
                http.Error(w, fmt.Sprintf("Error encoding response %v", err),
                           http.StatusInternalServerError)
        } else {
                w.Header().Set("Content-Type", "application/json")
                if _, err := w.Write(json); err != nil {
                        log.Errorf("Error writing response %v", err)
                        http.Error(w, fmt.Sprintf("Error writing response: %v", err),
                                   http.StatusInternalServerError)
                }
        }
}

2.4 Gestion des règles

2.4.1 Exemple de règles

La fonction checkRequest() doit donc prendre une décision : faut-il ou non autoriser l’opération ? Comme nous ne voulons pas coder « en dur » une règle comme « il est interdit de détruire les Nodes qui ne possèdent pas le label can-be-deleted=true », nous souhaitons externaliser les règles pour les fournir à notre Webhook dynamiquement lors de son lancement.

Nous proposons de définir les règles au format YAML. Il y aura 2 types de règles :

  • les must rules qui définiront les objets qui devront posséder un certain label pour pouvoir être détruits ;
  • les must-not rules qui définiront les objets qui ne devront pas posséder un certain label pour pouvoir être détruits.

Prenons un exemple de must rules :

- namespace: default
  kinds:
    - Pod
    - Deployment
  label: allowed-for-deletion
- namespace: "*"
  kinds:
    - Node
  label: allowed-for-deletion

La première règle interdira la destruction de Pod et Deployment du Namespace default s’il ne possède pas le label allowed-for-deletion.

La deuxième règle interdira la destruction de tout Node ne possédant pas ce même label.

Prenons un exemple de must-not rules :

- namespace: "*"
  kinds:
    - "*"
  label: protected-against-deletion

Cette règle unique interdira la destruction de tout objet, quel que soit son Namespace, s’il possède le label protected-against-deletion.

2.4.2 Chargement des règles

Pour charger les fichiers de règles, on ajoute le code suivant dans la fonction main() :

        ...
        flag.StringVar(&must_rules_filename, "must-rules", "must.rules",
                "YAML file containing the 'must' rules")
        flag.StringVar(&must_not_rules_filename, "must-not-rules", "must-not.rules",
                "YAML file containing the 'must-not' rules")
 
        flag.Parse()
 
        must_rules = load_rules_file(must_rules_filename)
        must_not_rules = load_rules_file(must_not_rules_filename)

Il faut lire le contenu des fichiers de règles écrites en YAML et les transformer en structure de données. Cette opération est réalisée par la fonction load_rules_file() définie dans le fichier rules.go :

import (
        log "github.com/sirupsen/logrus"
        "gopkg.in/yaml.v2"
        "io/ioutil"
)
 
// Rules are "must" or "must_not" but have the same syntax
type Rule struct {
        Namespace string `yaml:"namespace"`
        Kinds []string `yaml:"kinds"`
        Label string    `yaml:"label"`
}
 
func load_rules_file(filename string) []Rule {
        var rules []Rule
        data, err := ioutil.ReadFile(filename)
        if err != nil {
                log.Fatalf("Could not read file %s: %v", filename, err)
        }
 
        err = yaml.UnmarshalStrict([]byte(data), &rules)
        if err != nil {
                log.Fatalf("YAML error: %v", err)
        }
        log.Debugf("Rules for file(%s): %v", filename, rules)
 
        return rules
}

2.4.3 Application des règles

À cette étape, nous avons les variables must_rules et must_not_rules qui contiennent les règles à appliquer par la fonction checkRequest().

Le code n’est pas très complexe. Tout d’abord, on élimine les requêtes qui ne sont pas des DELETE d’objets. Ensuite, on applique les must rules : si une règle interdit la destruction, on le signale à l’Apiserver. Puis, on fait de même avec les must-not rules.

func checkRequest(request *admission.AdmissionRequest) (bool, error) {
        if request.Operation != "DELETE" {
                return true, nil
        }
 
        // Apply "must" rules
        for idx, rule := range must_rules {
                if doesRuleApply(&rule, request) {
                        // The rule.label must exist !
                        if labels, err := getObjectLabels(request); err == nil {
                                if _, present := labels[rule.Label]; !present {
                                        return false, fmt.Errorf("Object must not be deleted because it does not have this label: %s", rule.Label)
                                }
                        }
                }
        }
 
        // Apply "must-not" rules
        for idx, rule := range must_not_rules {
                if doesRuleApply(&rule, request) {
                        // The rule.label must not exist !
                        if labels, err := getObjectLabels(request); err == nil {
                                if _, present := labels[rule.Label]; present {
                                        return false, fmt.Errorf("Object must not be deleted because it has this label: %s", rule.Label)
                                }
                        }
                }
        }
 
        return true, nil
}

La fonction doesRuleApply() retourne un booléen qui indique si la règle passée en paramètre s’applique à la requête de l’Apiserver. Pour cela, on compare les Namespaces et les types des objets (Kinds) :

func doesRuleApply(rule *Rule, request *admission.AdmissionRequest) bool {
        // Rule syntax:
        // namespace: default
        // kinds:
        //   - pods
        //   - nodes
        // label: allowed-for-deletion
 
        // namespace must match
        if rule.Namespace != "*" && (rule.Namespace != request.Namespace) {
                return false
        }
 
        // kind must match
        match := false
        for _, kind := range rule.Kinds {
                if kind == "*" || kind == request.Kind.Kind {
                        match = true
                        break
                }
        }
        if !match {
                return false
        }
 
        return true
}

Si une règle s’applique à une requête de l’Apiserver, la fonction checkRequest() doit obtenir les labels de l’objet examiné en appelant la fonction getObjectLabels() :

func getObjectLabels(request *admission.AdmissionRequest) (map[string]string, error) {
        var result map[string]interface{}
        var metadata map[string]interface{}
        labels := make(map[string]string)
 
        // Try to get the object label without taking care of the object type (Pod, Node, ...)
        if err := json.Unmarshal(request.OldObject.Raw, &result); err != nil {
                log.Errorf("Could not unmarshal raw object: %v", err)
                return labels, err
        }
 
        metadata = result["metadata"].(map[string]interface{})
 
        for key, value := range metadata["labels"].(map[string]interface{}) {
                labels[key] = value.(string)
        }
 
        return labels, nil
}

Notons que cette fonction obtient les labels de tout type d’objets grâce à une conversion générique des metadata de l’objet. En effet, il aurait été beaucoup trop fastidieux de traiter spécifiquement chaque type d’objets (les Pods, les Deployments, les StatefulSets, etc.).

3. Intégration dans le cluster Kubernetes

3.1 Création de l’image du Container

Comme notre Admission Webhook est un processus, il semble évident de l’encapsuler dans un Container pour le lancer en tant que Pod !

Le Dockerfile ci-dessous va compiler notre programme et copier le binaire obtenu sous /go/bin/k8s-delete-protection. Les paramètres nécessaires au lancement seront définis dans la spécification du Pod.

FROM golang:1.17.2 as builder
 
WORKDIR $GOPATH/src/github.com/majeinfo/k8s-delete-protection
COPY go.mod .
COPY go.sum .
 
RUN go mod download
 
COPY . .
 
RUN cd main && go test
RUN cd main && CGO_ENABLED=0 go build -o /go/bin/k8s-delete-protection
 
FROM scratch
COPY --from=builder /go/bin/k8s-delete-protection /go/bin/k8s-delete-protection
ENTRYPOINT ["/go/bin/k8s-delete-protection"]

Les commandes de build et push utilisées sont les suivantes :

$ docker build -t majetraining/k8s-delete-protection:v0.1 .
$ docker push majetraining/k8s-delete-protection:v0.1

3.2 Configuration de l’Admission Webhook

Au paragraphe 2.4, nous avons expliqué que les règles à appliquer seraient fournies au processus sous forme de fichiers. Kubernetes propose une solution idéale pour cela : les ConfigMaps ! Nous allons donc créer une ConfigMap contenant 2 clés. Une clé must et une clé must-not. Leur contenu sera l’équivalent des fichiers YAML présentés précédemment :

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: k8s-delete-protection
  namespace: kube-system
data:
  must: |
    - namespace: default
      kinds:
        - Pod
        - Deployment
      label: allowed-for-deletion
    - namespace: "*"
      kinds:
        - Node
      label: allowed-for-deletion
  must-not: |
    - namespace: "*"
      kinds:
        - "*"
      label: protected-against-deletion

Nous pouvons créer cette ConfigMap :

$ kubectl apply – f manifests/configmap.yaml
configmap/k8s-delete-protection created

De la même manière, nous devons passer à notre programme sa clé privée et son certificat. Comme ce sont des données sensibles, nous allons les embarquer dans un Secret :

# secrets-ca.yaml
apiVersion: v1
kind: Secret
metadata:
  name: k8s-delete-protection-certs
  namespace: kube-system
data:
  server-key.pem: $(cat certs/server-key.pem | base64 | tr -d '\n')
  server.pem: $(cat certs/server.crt | base64 | tr -d '\n')

Attention, remplacez les valeurs de server-key.pem et server.pem par le résultat de l’exécution de la commande qui est indiquée !

Nous créons le Secret ainsi :

$ kubectl apply – f manifests/secrets-ca.yaml
secret/k8s-delete-protection-certs created

3.3 Déploiement de l’Admission Webhook

Notre Admission Webhook est paré pour son activation ! Nous définissons un objet de type Deployment pour assurer sa surveillance.

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: k8s-delete-protection
  name: k8s-delete-protection
  namespace: kube-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app: k8s-delete-protection
  strategy: {}
  template:
    metadata:
      labels:
        app: k8s-delete-protection
    spec:
      containers:
      - name: admission
        image: majetraining/k8s-delete-protection:v0.1
        imagePullPolicy: IfNotPresent
        args:
        - -cert
        - /certs/server.pem
        - -key
        - /certs/server-key.pem
        - -must-rules
        - /config/must
        - -must-not-rules
        - /config/must-not
        - -verbose
        ports:
        - containerPort: 8443
        volumeMounts:
        - name: admission-certs
          mountPath: /certs
          readOnly: true
        - name: configmap
          mountPath: "/config"
          readOnly: true
        livenessProbe:
          httpGet:
            path: /health/liveness
            port: 8443
            scheme: HTTPS
          initialDelaySeconds: 5
          periodSeconds: 30
      volumes:
      - name: admission-certs
        secret:
          secretName: k8s-delete-protection-certs
      - name: configmap
        configMap:
          name: k8s-delete-protection

Remarquez comment nous avons « mappé » la ConfigMap sur le répertoire /config et le Secret sur le répertoire /certs. Créons le Deployment :

$ kubectl apply -f deployment.yaml
deployment.apps/k8s-delete-protection created

3.4 Configuration de l’Apiserver

Il nous reste un dernier lien à définir : le lien entre l’Apiserver et notre Admission Webhook !

Comme l’Apiserver doit invoquer un Service, il faut commencer par en définir un. Ce sera un service de type ClusterIP qui va rediriger le port 443/TCP sur le port 8443/TCP d’un des Pods qui encapsule notre Webhook (dans notre cas, le Deployment n’a lancé qu’un seul Pod, mais on aurait pu appliquer un facteur de réplication).

# service.yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: k8s-delete-protection
  name: k8s-delete-protection
  namespace: kube-system
spec:
  ports:
  - name: 443-8443
    port: 443
    protocol: TCP
    targetPort: 8443
  selector:
    app: k8s-delete-protection
  type: ClusterIP

Créons ce Service :

$ kubectl apply -f manifests/service.yaml
service/k8s-delete-protection created

Ultime étape d’intégration, il faut créer un objet de type ValidatingWebhookConfiguration pour configurer l’Apiserver :

# webhook.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: k8s-delete-protection.kube-system.cluster.local
namespace: kube-system
webhooks:
- name: k8s-delete-protection.kube-system.cluster.local
  clientConfig:
    service:
      name: k8s-delete-protection
      namespace: kube-system
      path: "/validate"
    caBundle: $(cat certs/ca.pem | base64 | tr -d '\n')
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["nodes", "pods" ]
    operations: ["DELETE"]
    scope: "*"
  - apiGroups: ["apps"]
    apiVersions: ["v1"]
    resources: ["deployments" ]
    operations: ["DELETE"]
    scope: "*"
  #namespaceSelector:
  # matchExpressions:
  # - key: name
  #    operator: In
  #    values: ["default"]
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Ignore

Que nous dit ce fichier ? Tout d’abord, notre Webhook est joignable via le nom de service k8s-delete-protection qui fait partie du Namespace kube-system. On retrouve aussi le nom de l’URL à invoquer : /validate.

Il faut aussi donner la valeur du certificat de l’autorité qui a signé le certificat qui sera présenté par notre Webhook. Pensez à remplacer la valeur de la clé caBundle par le contenu du certificat de l’AC !

Pour des raisons d’optimisation, nous ne voulons pas que l’Apiserver appelle systématiquement notre Webhook, nous pouvons pour cela filtrer les appels via la définition des rules.

Dans notre cas, nous ne voulons recevoir que les AdmissionReview concernant les destructions de Nodes et de Pods (ce sont des objets gérés par l’API core, donc on spécifie une chaîne vide dans la clé apiGroups) et nous voulons recevoir les demandes de destruction des Deployments (objets gérés par l’API apps).

Pour être complet, indiquons que le paramètre failurePolicy a pour valeurs possibles Fail ou Ignore. Il indique à l’Apiserver comment se comporter en cas d’erreur inconnue ou de timeout retourné par notre Webhook. Le paramètre sideEffects a pour valeur None, ce qui indique que notre Webhook n’impacte que l’objet indiqué par la structure AdmissionReview.

Appliquons la définition de notre WebHook :

$ kubectl apply -f webhook.yaml
validatingwebhookconfiguration.admissionregistration.k8s.io/k8s-delete-protection created

4. Tests & exemples d’utilisation

Si les règles que nous avons appliquées sont bien prises en compte, alors nous pouvons créer un Pod dans le Namespace default et nous ne devrions pas être capables de le détruire… facilement :

$ kubectl run nginx --image=nginx
pod/nginx created
 
$ kubectl get po
NAME                                READY   STATUS    RESTARTS   AGE
nginx                               1/1     Running   0          3m44s
 
$ kubectl delete po nginx
Error from server (Forbidden): admission webhook "k8s-delete-protection.kube-system.cluster.local" denied the request: Object must not be deleted because it does not have this label: allowed-for-deletion

Bingo ! Ça marche. Vérifions qu’en ajoutant le label, on peut détruire le Pod :

$ kubectl label po nginx allowed-for-deletion=true
pod/nginx labeled
 
$ kubectl delete po nginx
pod "nginx" deleted

Puisque le test est concluant, essayons de détruire un Node !

$ kubectl delete no ip-172-30-1-29.eu-west-1.compute.internal
Error from server (Forbidden): admission webhook "k8s-delete-protection.kube-system.cluster.local" denied the request: Object must not be deleted because it does not have this label: allowed-for-deletion

Notre but initial est atteint, personne ne pourra plus détruire un Node par erreur !

Les règles de type must-not permettent de protéger tout objet qui possède le label protected-against-deletion. Mais attention ! Rappelez-vous que nous avons configuré l’Apiserver pour qu’il ne contacte notre Webhook que pour la destruction des Pods, des Nodes et des Deployments ! Nous allons donc protéger contre la destruction un Deployment qui ne fait pas partie du Namespace default. Dans notre cas, c’est le metrics-server :

$ kubectl label deploy metrics-server -n kube-system protected-against-deletion=true
deployment.apps/metrics-server labeled
 
$ kubectl delete deploy metrics-server -n kube-system
Error from server (Forbidden): admission webhook "k8s-delete-protection.kube-system.cluster.local" denied the request: Object must not be deleted because it has this label: protected-against-deletion

Conclusion

Êtes-vous maintenant convaincu qu’étendre Kubernetes à l’aide de nouveaux Admissions Controllers n’est pas une tâche difficile ? Si vous consultez le code (disponible sur GitHub [1]), vous constaterez que nous y avons inclus une Liveness Probe (/health/liveness) qui pourra être invoquée par kubelet pour relancer le Controller en cas de dysfonctionnement. Notez aussi que si vous souhaitez modifier les règles, vous devrez modifier la ConfigMap et relancer de nouveaux Pods. En effet, notre Controller ne gère pas dynamiquement les modifications apportées à la ConfigMap... mais il existe des Custom Controllers qui rendent cela possible !

Références

[1] Dépôt GitHub du projet : https://github.com/majeinfo/k8s-delete-protection

[2] Lire à ce sujet l’article « Codez un Custom Controller pour Kubernetes » paru dans GNU/Linux Magazine n°243 : https://connect.ed-diamond.com/GNU-Linux-Magazine/glmf-243/creer-un-custom-resource-controller-pour-kubernetes

[3] Documentation du Webhook de validation :
https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/



Article rédigé par

Par le(s) même(s) auteur(s)

Le stockage de séries chronologiques avec InfluxDB

Magazine
Marque
Linux Pratique
HS n°
Numéro
53
Mois de parution
février 2022
Spécialité(s)
Résumé

Depuis une dizaine d’années, le mouvement NoSQL s’est largement répandu et de nouveaux types de bases de données sont apparus. Parmi celles-ci, les bases de données dites « orientées-séries-chronologiques » (TSDB pour Time Series Database) ont montré leur intérêt pour stocker et analyser des données horodatées. On les retrouve dans différents domaines : de l’Internet des objets (IoT) à la collecte de métriques serveurs et réseau, en passant par la surveillance d’applications, la mesure de performances… Dans ce marché de niche, InfluxDB apparaît comme une solution leader [1].

Plus sûr et plus simple que Docker, connaissez-vous Singularity ?

Magazine
Marque
Linux Pratique
Numéro
128
Mois de parution
novembre 2021
Spécialité(s)
Résumé

Que vous soyez un développeur, un DevOps ou un administrateur système, vous n’avez pas échappé à la « révolution des conteneurs », et parmi les solutions de conteneurisation disponibles vous avez probablement opté pour Docker ! Mais êtes-vous sûr que Docker est toujours la meilleure solution ? La plus adaptée à vos utilisateurs, à vos contraintes de sécurité ? Nous vous proposons de découvrir Singularity comme alternative à Docker.

Comment tester un rôle Ansible avec Molecule ?

Magazine
Marque
Linux Pratique
Numéro
128
Mois de parution
novembre 2021
Spécialité(s)
Résumé

Depuis quelques années Ansible est devenu la référence des outils d’automatisation. Ansible applique des Playbooks au format YAML sur vos machines virtuelles, vos équipements réseau, vos conteneurs, vos clusters Kubernetes... bref, il se veut universel. Mais rapidement, même avec une infrastructure modeste, vous devez organiser vos Playbooks. Pour cela, vous utilisez des Rôles que souvent vous développez vous-même. Ces Rôles vont forcément évoluer et dès lors comment être sûr qu’ils seront toujours fonctionnels et idempotents ? C’est là qu’intervient Molecule : cet outil va vous permettre de tester et valider vos Rôles et ainsi vous assurer que votre dépôt Git ne contiendra que des Rôles dûment opérationnels !

Les derniers articles Premiums

Les derniers articles Premium

Quarkus : applications Java pour conteneurs

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Initié par Red Hat, il y a quelques années le projet Quarkus a pris son envol et en est désormais à sa troisième version majeure. Il propose un cadre d’exécution pour une application de Java radicalement différente, où son exécution ultra optimisée en fait un parfait candidat pour le déploiement sur des conteneurs tels que ceux de Docker ou Podman. Quarkus va même encore plus loin, en permettant de transformer l’application Java en un exécutable natif ! Voici une rapide introduction, par la pratique, à cet incroyable framework, qui nous offrira l’opportunité d’illustrer également sa facilité de prise en main.

De la scytale au bit quantique : l’avenir de la cryptographie

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Imaginez un monde où nos données seraient aussi insaisissables que le célèbre chat de Schrödinger : à la fois sécurisées et non sécurisées jusqu'à ce qu'un cryptographe quantique décide d’y jeter un œil. Cet article nous emmène dans les méandres de la cryptographie quantique, où la physique quantique n'est pas seulement une affaire de laboratoires, mais la clé d'un futur numérique très sécurisé. Entre principes quantiques mystérieux, défis techniques, et applications pratiques, nous allons découvrir comment cette technologie s'apprête à encoder nos données dans une dimension où même les meilleurs cryptographes n’y pourraient rien faire.

Les nouvelles menaces liées à l’intelligence artificielle

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Sommes-nous proches de la singularité technologique ? Peu probable. Même si l’intelligence artificielle a fait un bond ces dernières années (elle est étudiée depuis des dizaines d’années), nous sommes loin d’en perdre le contrôle. Et pourtant, une partie de l’utilisation de l’intelligence artificielle échappe aux analystes. Eh oui ! Comme tout système, elle est utilisée par des acteurs malveillants essayant d’en tirer profit pécuniairement. Cet article met en exergue quelques-unes des applications de l’intelligence artificielle par des acteurs malveillants et décrit succinctement comment parer à leurs attaques.

Les listes de lecture

9 article(s) - ajoutée le 01/07/2020
Vous désirez apprendre le langage Python, mais ne savez pas trop par où commencer ? Cette liste de lecture vous permettra de faire vos premiers pas en découvrant l'écosystème de Python et en écrivant de petits scripts.
11 article(s) - ajoutée le 01/07/2020
La base de tout programme effectuant une tâche un tant soit peu complexe est un algorithme, une méthode permettant de manipuler des données pour obtenir un résultat attendu. Dans cette liste, vous pourrez découvrir quelques spécimens d'algorithmes.
10 article(s) - ajoutée le 01/07/2020
À quoi bon se targuer de posséder des pétaoctets de données si l'on est incapable d'analyser ces dernières ? Cette liste vous aidera à "faire parler" vos données.
Voir les 64 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous