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"); } } }
Per inserire un commento, devi avere un account.
Fai il login e torna a questa pagina, oppure registrati alla nostra community.
- C# e Net 6 in Kubernetes con Prometheus e Grafana, il 12 gennaio 2022 alle 21:58
- Snaturare Kubernetes evitando i custom container Docker, il 6 gennaio 2022 alle 19:40
- Provando Kaniko in Kubernetes come alternativa a Docker per la creazione di immagini, il 18 dicembre 2021 alle 20:11
- Divertissement con l'OpenID e Access Token, il 6 dicembre 2021 alle 20:05
- Operator per Kubernetes in C# e Net Core 6., il 28 novembre 2021 alle 19:44