RBAC in Kubernetes verso gli operator

di Andrea Zani, in .NET,

All'interno dei Pod nel cluster è possibile comunicare con le API di Kubernetes. Questo permette di avere informazioni sulle risorse attualmente allocate, crearne di nuove, modificarle e cancellarle. All'interno del Pod queste sono accessibile con chiamare https:

https://kubernetes/api/v1/...

Maggiori info qui e qui. Di default ovviamente il tutto è disabilitato. Un ottimo post che spiega il tutto (molto meglio di quanto io riuscirei a fare) lo si trova a questo url. Senza dover usare le chiamate API Rest direttamente è possibile utilizzare librerie già pronte che semplificano il tutto. Per esempio questa:

https://github.com/kubernetes-client/csharp

Una volta incluso nel proprio progetto in Net Core, sarà possibile operare con queste api con degli oggetti in C#. La prima fase è ovviamente autenticarsi:

var config = KubernetesClientConfiguration.InClusterConfig();
var client = new Kubernetes(config);

Ora, per esempio, per avere la lista dei namespace e dei Pod relativi nel proprio cluster:

Console.WriteLine("Namespace list:");
var namespaces = client.ListNamespace();
foreach (var ns in namespaces.Items)
{
  Console.WriteLine(ns.Metadata.Name);
  var list = client.ListNamespacedPod(ns.Metadata.Name);
  foreach (var item in list.Items)
  {
    Console.WriteLine($"- {item.Metadata.Name}");
  }

  Console.WriteLine();
}

Per creare un nuovo namespace:

var ns = new V1Namespace
{
  Metadata = new V1ObjectMeta
  {
    Name = "test"
  }
};

var result = client.CreateNamespace(ns);

Per cancellarlo:

var status = client.DeleteNamespace(ns.Metadata.Name, new V1DeleteOptions()); // in Name c'è il nome del namespace appena creato

Per fare qualcosa di più complesso, come creare un nuovo deploy:

var deploy = new V1Deployment
{
  Metadata = new V1ObjectMeta
  {
    Name = "nginx-deployment"
  },
  Spec = new V1DeploymentSpec
  {
    Selector = new V1LabelSelector
    {
      MatchLabels = new Dictionary<string, string>() { { "app", "nginx" } }
    },
    Replicas = 2,
    Template = new V1PodTemplateSpec
    {
      Metadata = new V1ObjectMeta
      {
        Labels = new Dictionary<string, string>() { { "app", "nginx" } }
      },
      Spec = new V1PodSpec
      {
        Containers = new List<V1Container>()
        {
          new V1Container
          {
            Name="nginx",
            Image="nginx:1.14.2",
            Ports = new List<V1ContainerPort>()
            {
              new V1ContainerPort { ContainerPort = 80 }
            }
          }
        }
      }
    }
  }
};

var result = await client.CreateNamespacedDeploymentAsync(deploy, "default");
Console.WriteLine($"Result from deploy: {result.Status}");

Deploy che creerà due nuovi Pod con nginx in esecuzione.

E' il momento di provare il tutto all'interno del cluster Kubernetes in locale. Per questo esempio userò la versione messa a disposizione all'interno del package di Docker per Windows 10. Dopo essermi creato un'immagine Docker con il codice qui sopra, pubblicherò questo job:

apiVersion: batch/v1
kind: Job
metadata:
  name: job-deploy
spec:
  completions: 1
  parallelism: 1
  template:
    metadata:
      name: job-deploy
    spec:
      containers:
      - name: mytest
        image: sbraer/apideploy # mia immagine 
      restartPolicy: OnFailure

Una volta avviata con:

kubectl create -f job1.yaml

Sarà creato il Pod e subito chiuso, e controllando troverò:

>kubectl get po
NAME                                READY   STATUS              RESTARTS   AGE
job-deploy-t9whg                    1/1     Running             0          16s
nginx-deployment-66b6c48dd5-k7mqm   0/1     ContainerCreating   0          0s
nginx-deployment-66b6c48dd5-rhplj   0/1     ContainerCreating   0          0s

Trovandomi già i Pod di nginx so già che ha funzionato tutto. Ma voglio maggiori dettagli ed estraggo il log del mio Pod:

>kubectl logs job-deploy-t9whg
Create new Deploy

Namespace list:
default
- job-deploy-t9whg

kube-node-lease

kube-public

kube-system
- coredns-558bd4d5db-ncbxl
- coredns-558bd4d5db-rcpbr
- etcd-docker-desktop
- kube-apiserver-docker-desktop
- kube-controller-manager-docker-desktop
- kube-proxy-t7vlg
- kube-scheduler-docker-desktop
- storage-provisioner
- vpnkit-controller

Create new namespace
k8s.Models.V1NamespaceStatus
Delete namespace


Create new Deploy
Result from deploy: k8s.Models.V1DeploymentStatus

Completed

In cui vedo la lista dei namespace nel mio cluster più le varie operazioni di creazione e cancellazione. Tutto ha funzionato perfettamente, ma qualcosa non torna. Come ho scritto all'inizio di default NON dovrebbe essere possile accedere alle API di K8s, perché quindi ha funzionato? Rifacendo la stessa prova su un cluster reale di K8s mi riporta all'amara realtà: mi ritrovo un solo Pod il cui log:

>kubectl logs job-deploy-jpr4g
Create new Deploy

Namespace list:
Phase: Forbidden
Content: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"namespaces is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"namespaces\" in API group \"\" at the cluster scope","reason":"Forbidden","details":{"kind":"namespaces"},"code":403}

Create new namespace
Phase: Forbidden
Content: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"namespaces is forbidden: User \"system:serviceaccount:default:default\" cannot create resource \"namespaces\" in API group \"\" at the cluster scope","reason":"Forbidden","details":{"kind":"namespaces"},"code":403}

Create new Deploy
Phase: Forbidden
Content: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"deployments.apps is forbidden: User \"system:serviceaccount:default:default\" cannot create resource \"deployments\" in API group \"apps\" in the namespace \"default\"","reason":"Forbidden","details":{"group":"apps","kind":"deployments"},"code":403}

Completed

Ora tutto quadra. Gli errori Forbidden/403 sono quanto di più normale dovevo aspettarmi. Confesso che non so spiegare perché la versione di K8s in Docker in locale ha questo funzionamento (ho controllato e il modulo RBAC è in esecuzione correttamente). Se provo a fare la stessa cosa con Minikube, infatti, ottengo la risposta come la precedente - Fobidden etc... La cosa non mi tange e proseguo, ma è sempre meglio saperlo.

E' ora arrivato il momento di dare i permessi per l'accesso alle API di K8s. Per fare questo mi creo un service account in K8s grazie al quale potrò fare le mie operazioni sulle risorse.

Primo passo è creare un service account:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: website-controller
  namespace: default

Ora creo una ClusterRole per i permessi per la gestione dei namespace:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: website-controller-namespaces
rules:
- apiGroups: [""]
  resources: ["pods", "namespaces"]
  verbs: ["list", "create", "delete"]

Quindi collego questa role al service account:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: allow-namespace
subjects:
- kind: ServiceAccount
  name: website-controller
  namespace: default
roleRef:
  kind: ClusterRole
  name: website-controller-namespaces
  apiGroup: rbac.authorization.k8s.io

Ci sono due tipi di role in K8s: Role e ClusterRole. Io ho usato il secondo le cui spefiche avranno visiblità in tutto il cluster mentre il primo solo ad un determinato namespace.

Ed ecco la role per il deploy e il collegamento con il service account:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: website-controller-deployment
rules:
- apiGroups: ["","apps"]
  resources: ["deployments"]
  verbs: ["create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: allow-deployment
subjects:
- kind: ServiceAccount
  name: website-controller
  namespace: default
roleRef:
  kind: ClusterRole
  name: website-controller-deployment
  apiGroup: rbac.authorization.k8s.io

Piccola premessa prima del test finale: a parte leggere la documentazione è possibile, in caso di errori di autorizzazioni, scoprire cosa manca alla configurazione controllando nel dettaglio il messaggio di errore da parte delle API. Nel mio codice questo lo faccio con questa Exception:

catch (Microsoft.Rest.HttpOperationException httpOperationException)
{
  var phase = httpOperationException.Response.ReasonPhrase;
  var content = httpOperationException.Response.Content;
  Console.WriteLine($"Phase: {phase}\r\nContent: {content}");
}

Nel caso visto prima del deploy ritorna questo errore:

"deployments.apps is forbidden: User \"system:serviceaccount:default:default\" cannot create resource \"deployments\" in API group \"apps\" in the namespace \"default\"","reason":"Forbidden","details":{"group":"apps","kind":"deployments"},"code":403}

In cui si legge che il service account usato di default non può creare il deployment con ulteriori info sul group e il kind da usare, da qui è facile scrivere la role vista prima:

apiGroups: ["","apps"]
resources: ["deployments"]
verbs: ["create"]

E' il momento di verificare che tutto funzioni. Nel file di configurazione del job devo aggiungere il service account da utilizzare (website-controller):

apiVersion: batch/v1
kind: Job
metadata:
  name: job-deploy
spec:
  completions: 1
  parallelism: 1
  template:
    metadata:
      name: job-deploy
    spec:
      serviceAccountName: website-controller
      containers:
      - name: mytest
        image: sbraer/apideploy
      restartPolicy: OnFailure

Eseguito anche sul cluster reale tutto funzionerà senza trucchi e inganni. Ma cosa è servito tutto questo? Quanto imparato oggi sarà utilissimo nella creazione degli Operator in K8s su cui, chissà quando, scriverò qualcosa.

Qui il codice in C# usato dal mio programma di test presente nell'immagine Docker:

using k8s;
using k8s.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace ApiDeploy
{
  class Program
  {
    static async Task Main(string[] _)
    {
      Console.WriteLine("Create new Deploy");
      Console.WriteLine();

      try
      {
        var config = KubernetesClientConfiguration.InClusterConfig();

        // Use the config object to create a client.
        var client = new Kubernetes(config);

        try
        {
          Console.WriteLine("Namespace list:");
          var namespaces = client.ListNamespace();
          foreach (var ns in namespaces.Items)
          {
            Console.WriteLine(ns.Metadata.Name);
            var list = client.ListNamespacedPod(ns.Metadata.Name);
            foreach (var item in list.Items)
            {
              Console.WriteLine($"- {item.Metadata.Name}");
            }

            Console.WriteLine();
          }

          Console.WriteLine();
        }
        catch (Microsoft.Rest.HttpOperationException httpOperationException)
        {
          var phase = httpOperationException.Response.ReasonPhrase;
          var content = httpOperationException.Response.Content;
          Console.WriteLine($"Phase: {phase}\r\nContent: {content}");
        }
        catch (Exception ex)
        {
          Console.WriteLine(ex.Message);
        }

        try
        {
          Console.WriteLine("Create new namespace");
          var ns = new V1Namespace
          {
            Metadata = new V1ObjectMeta
            {
              Name = "test"
            }
          };

          var result = client.CreateNamespace(ns);
          Console.WriteLine(result.Status);

          Console.WriteLine("Delete namespace");
          var status = client.DeleteNamespace(ns.Metadata.Name, new V1DeleteOptions());
          Console.WriteLine(status);
          Console.WriteLine();
        }
        catch (Microsoft.Rest.HttpOperationException httpOperationException)
        {
          var phase = httpOperationException.Response.ReasonPhrase;
          var content = httpOperationException.Response.Content;
          Console.WriteLine($"Phase: {phase}\r\nContent: {content}");
        }
        catch (Exception ex)
        {
          Console.WriteLine(ex.Message);
        }

        try
        {
          Console.WriteLine("Create new Deploy");
          var deploy = new V1Deployment
          {
            Metadata = new V1ObjectMeta
            {
              Name = "nginx-deployment"
            },
            Spec = new V1DeploymentSpec
            {
              Selector = new V1LabelSelector
              {
                MatchLabels = new Dictionary<string, string>() { { "app", "nginx" } }
              },
              Replicas = 2,
              Template = new V1PodTemplateSpec
              {
                Metadata = new V1ObjectMeta
                {
                  Labels = new Dictionary<string, string>() { { "app", "nginx" } }
                },
                Spec = new V1PodSpec
                {
                  Containers = new List<V1Container>()
                  {
                    new V1Container
                    {
                      Name="nginx",
                      Image="nginx:1.14.2",
                      Ports = new List<V1ContainerPort>()
                      {
                        new V1ContainerPort { ContainerPort = 80 }
                      }
                    }
                  }
                }
              }
            }
          };

          var result = await client.CreateNamespacedDeploymentAsync(deploy, "default");
          Console.WriteLine($"Result from deploy: {result.Status}");
          Console.WriteLine();
        }
        catch (Microsoft.Rest.HttpOperationException httpOperationException)
        {
          var phase = httpOperationException.Response.ReasonPhrase;
          var content = httpOperationException.Response.Content;
          Console.WriteLine($"Phase: {phase}\r\nContent: {content}");
        }
        catch (Exception ex)
        {
          Console.WriteLine(ex.Message);
        }
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message, ex);
      }

      Console.WriteLine("Completed");
    }
  }
}
Commenti

Visualizza/aggiungi commenti

| Condividi su: Twitter, Facebook, LinkedIn

Per inserire un commento, devi avere un account.

Fai il login e torna a questa pagina, oppure registrati alla nostra community.

Nella stessa categoria
I più letti del mese