L'idea di questo post è nata dopo una discussione sui vantaggi e svantaggi nel mantenere un cluster Kubernetes per l'esecuzione di più servizi. In effetti non c'è nessuno che obbliga a questa scelta quando si utilizzano altri strumenti performanti e affidabili.
Forse sono partito male ed è meglio partire dall'inizio. Lo scenario è il seguente: dei servizi girano su alcuni server (alcuni di essi servizi scritti in Python) e vengono gestiti grazie ad Ansible. Semplificando il tutto, ad una macchina in cui gira Ansible sono collegati altri nodi su cui vengono installati ed eseguiti dei servizi proprio grazie ad Ansible. Ah dimenticavo la prima mia premessa: Ansible io lo conosco solo in teoria e non ho mai avuto la fortuna di poterci mettere le mani seriamente. Proseguo: da quel che ho capito, questi script in Ansible scaricano da un repository il codice sorgente sulla macchina interessata e ne lanciano l'esecuzione, fine.
La contestazione nell'uso di Kubernetes in questo scenario è la seguente: ma perché dovrei complicarmi la vita aggiungendo un ulteriore layer di complessità nel Deploy visto che si deve creare un container Docker prima di poterlo eseguire nel cluster?
Da questa semplice domanda sono seguite una lunga lista di problematiche annesse e investigazioni già fatte dall'autore di questa contestazione (al quale non ho potuto rispondere perché non conosco Python in ambito Docker). Uno dei punti fondamentali è la dimensione dell'immagine creata per ogni servizio scritto in Python che soffre dello stesso problema di .Net (Core): doversi trascinare dietro tutte le dipendenze che nel caso di Python è proprio il suo eseguibile e nel caso di .Net è tutto il Framework (tralasciando la possibilità di compilare entrambe in modo che possano essere eseguiti senza alcun supporto esterno). A parte eventuali trucchi che portano ad una dimensione ridotta dell'immagine ridotta, è il concetto di base dell'uso di Docker per ogni build che rendeva il contestatore contrario all'uso di strumenti come Kubernetes.
Seconda premessa: il continuo della discussione e questo post NON sono in alcun modo un motivo per convincere chicchessia della bontà di Kubernetes a confronto di altre soluzione (come Ansible, visto che l'ho tirato in ballo).
Ovviamente c'è un trucco che permette di eseguire qualsiasi codice senza dover creare immagini Docker, ed è molto semplice, e si basa, nel caso di Python, di scaricare da git, per esempio, il proprio codice e farlo girare in un container in cui è presente il comando Python. Fine. Così come per le applicazioni in C# con .Net: è sufficiente utilizzare l'immagine contenente l'SDK, scaricare il codice sorgente, compilarlo, ed eseguirlo. In caso di aggiornamento del codice sorgente, è sufficiente riavviare il Deploy in Kubernetes dell'applicazione e ritrovare in esecuzione l'ultima versione del nostro codice. Ovviamente in tutto questo ci sono sì dei vantaggi, ma pure degli svantaggi che tratterò poi. Parto con l'esempio in Python perché era il principale soggetto della discussione. Io utilizzerò il seguente codice che crea un web server in Python e mostra delle semplici pagine html:
# importazione delle librerie necessarie import http.server import socketserver import os PORT = 8000 ROOT_FOLDER = 'wwwroot' web_dir = os.path.join(os.path.dirname(__file__), ROOT_FOLDER) os.chdir(web_dir) Handler = http.server.SimpleHTTPRequestHandler httpd = socketserver.TCPServer(("", PORT), Handler) print("serving at port", PORT) httpd.serve_forever()
Maggiori info qui. Ora che ho il web server devo pubblicarlo con Kubernetis. Non creerò l'immagine contenente questo codice ma utilizzerò python:3.8-slim-buster come immagine nel container in cui lo eseguirò. A questo punto il problema è come fare trovare il codice qui sopra all'interno di questo container. In aiuto vengono gli initContainers. Prima dell'avvio del/dei container del Pod, è possibile avviare uno o più container che, con il Pod principale, condivideranno tutto, compreso un eventuale volume collegato e, finito il loro compito, si cancellano liberando risorse del cluster. Per la mia necessità creerò un initContainer che scaricherà il codice sorgente qui sopra e lo preparerà per il container principale nel Pod che penserà alla sua esecuzione. Ecco qui un Deployment di Kubernetes che fa quanto scritto finora:
apiVersion: v1 kind: Service metadata: name: pythonservice2 spec: selector: app: pythonweb2 ports: - protocol: TCP port: 8084 targetPort: 8000 type: LoadBalancer --- apiVersion: apps/v1 kind: Deployment metadata: name: pythonweb2 labels: app: pythonweb2 spec: replicas: 1 selector: matchLabels: app: pythonweb2 template: metadata: labels: app: pythonweb2 spec: volumes: - name: data emptyDir: {} containers: - name: run image: python:3.8-slim-buster imagePullPolicy: IfNotPresent tty: true command: ["sh", "-c"] args: - echo Start1; cd /app; python server.py readinessProbe: httpGet: path: / port: 8000 periodSeconds: 10 initialDelaySeconds: 3 volumeMounts: - name: data mountPath: /app # These containers are run during pod initialization initContainers: - name: downloadcode image: bitnami/git:2.34.1 imagePullPolicy: IfNotPresent command: ["sh", "-c"] args: - echo Start; mkdir /apptmp; cd /apptmp; git clone https://github.com/sbraer/k8sdeploy.git; cd k8sdeploy/mypython; cp -Rf . /app; volumeMounts: - name: data mountPath: /app
In initContainer ho caricato un'immagine con git che mi permettesse di scaricare il codice sorgente, e nel Pod principale l'immagine con Python; entrambi questi container condividevano un Volume di tipo emptyDir (volume temporaneo che viene cancellato alla distruzione del Pod). Il container che scarica il codice sorgente lo inserisce in questo volume che sarà poi visto e avviato dal Container principale.
Avviato con il classico comando:
kubectl apply -f python-webapp.yaml
Atteso il tempo di scaricare le immagini e avviare il tutto sarà possibile vedere il risultato da browser con l'url: http://localhost:8084. L'esempio presente in questo post e tutti i riferimenti qui fatti sono frutto dei test con l'uso di Kubernetes presente nell'installazione standard di Docker in Windows 10. Se si usano altri cluster, verificare che il service utilizzato, LoadBalancer, abbia assegnato correttamente l'external IP:
kubectl get svc
Altrimenti, verificato il nome del Pod in cui gira Python, è possibile fare un port-forward diretto:
kubectl get po kubectl port-forward nomepo 8080
Se modificassi il file python-example.yaml, per esempio cambiassi la stringa di output echo Start1, e eseguito nuovamente il comando:
kubectl apply -f python-webapp.yaml
Un nuovo Pod sarà creato contenente l'ultima versione del codice, e quando tutto è pronto, il vecchio sarà cancellato. Con questo semplice trucco ho tolto di mezzo un attore nel Deploy di una nuova versione del codice: la build dell'immagine. Ma alla fine è un vantaggio? L'unico inconveniente nella creazione di un'immagine Docker è il suo tempo per la creazione; per il resto del Deploy, è solo un vantaggio. Alla fine solo la prima volta l'immagine creata in Docker potrebbe occupare anche parecchi megabyte, ma ai successi download per aggiornamenti al codice, solo il layer contenente le modifiche (solitamente pochissimi megabyte), saranno scaricati. L'utilizzo di questo trucco, invece, permette sì di evitare questo step del Deployment, ma comporta una certa lentezza della creazione e aggiornamento del Pod con il codice aggiornato (di seguito mostrerò lo stesso esempio con un'applicazione in Net6) che nel caso di altri linguaggi e tecnologie, come il Net Core, necessita di compilazione. Il contestatore non potrebbe essere contento di questa soluzione? Non so... pazienza.
Complichiamo un po' la cosa. Ora utilizzerò una piccola applicazione scritta in Net6 in un repository su Github privato, inoltre il codice dovrà essere compilato, e per rendere il tutto più difficile non userò l'user root in Docker come da default, ma un utente con permessi limitati - come si dovrebbe sempre fare. Primo passaggio: configurare il repository su Github in modo che possa permettere il download solo chi è autorizzato. Tralasciando la scelta delle credenziali pure (username e password), è possibile usare le Access Key con permessi di sola lettura. Prima di tutto sarà necessario create una ssh key. Se si utilizza Windows il modo più semplice è usare Docker con un container Linux:
$ docker run --rm -it bitnami/git:2.34.1 bin/bash root@2013cebbc2d0:/# ssh-keygen -b 4096 Generating public/private rsa key pair. Enter file in which to save the key (/root/.ssh/id_rsa): Created directory '/root/.ssh'. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /root/.ssh/id_rsa. Your public key has been saved in /root/.ssh/id_rsa.pub. The key fingerprint is: SHA256:geXu7kM+tCfFK4os65A8jQWsgvDxWIforSguSwkzJXg root@2013cebbc2d0 The key's randomart image is: +---[RSA 4096]----+ | . | |o . . + | |+oE o o o | |+*.* . . . | |B +.o S. | |+==. .o o | |=*.. +.o . | |=.o. . .B o | |o+ooo .oo* | +----[SHA256]-----+
Ho lasciato i valori di default e NON ho creato la passphrase. Ho lasciato che i file venissero creati nella directory di default ed ora ho bisogno della chiave pubblica che prendo con:
cat /root/.ssh/id_rsa.pub ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC1mNPK/Zr+dLikf0Lu471ZMzR2IvSisRWzYGtMDEHz0Hy72snvh/7vCFV8SFIE6xBmZmkCAx9aDJx0Eom+kDqYfI5dwDTVxG97Ix8IZ4gPXrILMJnYox4kWy/iBY1WTqU6+Q7wCe8UMavfYVs+yqPefFvW/fWiVRyoTsN3lHELTBsUHCR43HY5OTOu84Q/Fnn+J5QVhkZ/RUUA1R+xv7dIKRTa1wHPiqurPMCqdVR1Wii5LKxK1vO2f/RTydtDwkEWXoR6TXkgAD4iVDIGDES4ftVRkTRfa6fOagpX+e/4avJb9FOrQkntO2Au9bHP0Mn+sy7jW4BDtMzFlfn+y0MSppAg/QrgxJGHWIRwVoGvZvJgfLdQ44pi0xOYizCJe2S7xYQG9Qj1wKybEHbljMVsgWMHKJUu8ayO9cD1Q+YvTGJ9S408+DI3FGmgPh2lVmLtFh63rxV1rqwH7/dj5vH0WC1WQYMTtJPr4buZu/SrwrFfSYRgl4i6dvAPZfyq9WDAtFxCeLMGmtwCSo2LANdlJlAG9TBVQpZyb3Uqt9BLsu/Z+KAacKadVb1bNWL6tb+jnNXFA/ptJ0NFQWImkVwGXhLrnhVWLJPRtvQEn8qhwoUo9AyW5IAJl/qImGnoBp61sNn84BoBe1vUzS0KzM5Z/alkBdRjj03pFN2OYtOr7Q== root@2013cebbc2d0
Lasciando aperto questo container Docker, vado nella pagina in Github con il mio codice, quindi vado in Settings -> Deploy Keys -> Add Deploy Key:
Qui copio la stringa presa dal container Docker. Piccola nota, anche in Bitbucket è presente questa feature sotto Access Keys.
Ora ho bisogno della chiave privata che dovrò inserire in un Secret in Kubernetes in base64:
root@2013cebbc2d0:/# cat /root/.ssh/id_rsa | base64 -w 0 LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEU...IIFBSSVZBVEUgS0VZLS0tLS0K
Ora definisco il Secret in Kubernetes:
apiVersion: v1 kind: Secret metadata: name: keyforgit type: Opaque data: id_rsa: LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEU...IIFBSSVZBVEUgS0VZLS0tLS0K config: SG9zdCAqCiAgIFN0cmljdEhvc3RLZXlDaGVja2luZyBubw==
Oltre al file id_rsa ho inserito anche il file config che ha questo contenuto (il cui contenuto decodificato):
Host * StrictHostKeyChecking no
Per evitare che al momento del clone della soluzione venga richiesta la conferma per la chiave a cui non è possibile rispondere.
Prima di mostrare il file yaml completo per il Deploy della applicazione in Net6, un piccolo dettaglio di cui ci si deve occupare. Come scritto sopra ho deciso di eseguire questa applicazione con un utente con permessi limitati per maggiore sicurezza. Di default Docker avvia i suoi container con l'utente root con cui si può fare tutto, pure i danni. Sarebbe buona regola evitare questa cosa. Questo fa nascere un problema nei permessi dei file id_rsa e config visti prima che bloccheranno il loro uso. La soluzione è semplice: sempre da initContainer avvio un container con i permessi maggiori che, montato un volume con il contenuto di quel Secret, copia della giusta folder questi file assegnandone i giusti permessi (i file sono montati in /keys):
cp -Rf /keys/ /root/.ssh/; chmod 400 /root/.ssh/id_rsa; chmod 400 /root/.ssh/config; mkdir /app/apptmp/; cd /app/apptmp/; git clone git@github.com:sbraer/k8sdeploy.git;
Risolto questo problema sono pronto al deploy:
apiVersion: v1 kind: Secret metadata: name: keyforgit type: Opaque data: id_rsa: LS0tLS1CRUdJTiBPUEVOU...U1NIIFBSSVZBVEUgS0VZLS0tLS0K config: SG9zdCAqCiAgIFN0cmljdEhvc3RLZXlDaGVja2luZyBubw== --- apiVersion: v1 kind: Service metadata: name: minimalapiservice spec: selector: app: minimalapiweb ports: - protocol: TCP port: 8082 targetPort: 3000 type: LoadBalancer --- apiVersion: apps/v1 kind: Deployment metadata: name: minimalapiweb labels: app: minimalapiweb spec: replicas: 1 selector: matchLabels: app: minimalapiweb template: metadata: labels: app: minimalapiweb spec: securityContext: fsGroup: 2000 volumes: - name: gitlogin secret: secretName: keyforgit defaultMode: 256 - name: data emptyDir: {} containers: - name: run image: mcr.microsoft.com/dotnet/aspnet:6.0-alpine imagePullPolicy: IfNotPresent securityContext: runAsUser: 1000 runAsGroup: 3000 command: ["sh", "-c"] args: - echo Start 1; ls -R /app; cd /app/out; ./MinimalApiTestHost; readinessProbe: httpGet: path: / port: 3000 periodSeconds: 10 initialDelaySeconds: 3 volumeMounts: - name: data mountPath: /app readOnly: true # These containers are run during pod initialization initContainers: - name: downloadcode image: bitnami/git:2.34.1 imagePullPolicy: IfNotPresent command: ["sh", "-c"] args: - echo Start1; cp -Rf /keys/ /root/.ssh/; chmod 400 /root/.ssh/id_rsa; chmod 400 /root/.ssh/config; mkdir /app/apptmp/; cd /app/apptmp/; git clone git@github.com:sbraer/k8sdeploy.git; volumeMounts: - name: gitlogin mountPath: /keys - name: data mountPath: /app - name: build image: mcr.microsoft.com/dotnet/sdk:6.0-alpine imagePullPolicy: IfNotPresent securityContext: runAsUser: 0 # to use chmod runAsGroup: 0 # command: ["sh", "-c"] args: - echo Start1; cd /app/apptmp/; cd k8sdeploy/MinimalApiTestHost/; dotnet build -c Release -o /app/out; cd /app/out; rm -Rf /app/apptmp/; volumeMounts: - name: data mountPath: /app
Sono presenti due container nella sezione initContainer, il primo - downloadcode - che con il codice visto prima, esegue il clone del repository da Github. Quindi build che compilerà il mio codice e lo copierà nella directory /app/out:
cd /app/apptmp/; cd k8sdeploy/MinimalApiTestHost/; dotnet build -c Release -o /app/out; cd /app/out; rm -Rf /app/apptmp/;
Alla fine il container principale che userà come immagine mcr.microsoft.com/dotnet/aspnet:6.0-alpine (non l'SDK), avvierà la web app:
cd /app/out; ./MinimalApiTestHost;
Il codice di questa we bapp? Molto semplice:
using System.Net; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => $"Hello, World! v1.0: {Dns.GetHostName()}"); app.Run("http://*:3000");
Restituisce il classico messaggio Hello, World! e il nome dell'Host, che in questo caso è il nome del container in cui gira l'applicazione.
Eccomi ai commenti finali. Questo trucco funziona ed evita la creazione dell'immagine Docker e può andare bene come il test che ho presentato in questo post. Ho fatto delle prove creando decine di Pod in questo modo, e i tempi di Deploy si sono allungati, ovviamente. Inoltre la creazione dei Container per la compilazione ha fatto salire notevolmente il consumo delle risorse nel cluster, problema risolvibile con il RollingUpdate anche se con tempi ben superiori per l'update.
Alla fine questo post non voleva convincere nessuno. E' solo stato un altro mio divertissement.
Qui il codice. Per la creazione nel cluster Kubernetes l'applicazione in Python:
kubectl create -f python-webapp.yaml
Questa è per l'applicazione in Net6; comprende pure la configurazione per l'access Key in Github come esempio, anche se ora il codice è pubblico.
kubectl create -f minimalapi.yaml
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
- 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
- RBAC in Kubernetes verso gli operator, il 21 novembre 2021 alle 20:52