Continuiamo il nostro viaggio alla scoperta del deploy di un’applicazione Laravel su Kubernetes. Nell’articolo precedente, che vi consiglio di leggere se non l’avete già fatto, ho introdotto il tema parlando di come ho concepito il lavoro da fare e le motivazioni dietro le mie scelte.
Oggi, però, è il giorno in cui iniziamo a mettere le mani in pasta.
Quindi: cosa vedremo?
- partiremo con la creazione di tutte le risorse necessarie su DigitalOcean, il provider di servizi che ho scelto di usare. Nello specifico, creeremo insieme il cluster managed ed i due database (MySQL e Redis) che ho usato;
- daremo uno sguardo alla creazione delle immagini docker per l’app, che verranno usate dai pod del cluster;
- infine, inizieremo a mettere le mani concretamente su Kubernetes, creando i deployment e service per la nostra app;
Iniziamo!
Tiriamo su l’infrastruttura!
Il primo passo è creare l’infrastruttura di cui avremo bisogno. Come già detto precedentemente e nell’articolo prima di questo, avremo bisogno di:
- un cluster kubernetes managed, dove installeremo la nostra applicazione;
- un database MySQL managed, per memorizzare i dati dell’applicazione;
- un’istanza Redis managed, da usare come cache e per Laravel Horizon;
Ora, fare tutto questo con DigitalOcean è abbastanza semplice. Possiamo accedere alla creazione di un nuovo cluster selezionando, dal menu in alto a destra, Create > Kubernetes.
Da qui, tra le varie cose, sceglieremo:
- collocazione geografica del cluster;
- tipo e quantità di nodi da inserire;
Nel mio caso, ad esempio, ho creato un cluster, con una pool di tre nodi da 20€ al mese, a Londra. Per fare esperienza, chiaramente, basta anche meno. Quel “tre” è il numero minimo di nodi consigliato per un cluster in produzione. Una volta cliccato su “Create Cluster”, DigitalOcean inizia a creare tutto il necessario.
Nel frattempo passiamo ai database, scegliendo Create > Databases dallo stesso menu di prima.
Anche qui, esattamente come prima, non ho fatto altro che inserire i dettagli di cui avevo bisogno (collocazione del cluster, numero di nodi e così via).
Tempo un caffè ed è tutto pronto. Passiamo quindi a…
Creare l’Immagine Docker dell’Applicazione
Se non l’abbiamo già fatto in precedenza, dobbiamo creare l’immagine docker per il container della nostra applicazione. Sarà proprio quell’immagine, infatti, che useremo per andare a creare i pod di cui avremo bisogno su Kubernetes.
Inutile dire che qui bisogna necessariamente avere un po’ di dimestichezza con Docker.
Questa di seguito è l’immagine che ho usato per la mia app:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
FROM php:7.3-fpm WORKDIR /var/www RUN apt-get update && apt-get install -y libmcrypt-dev zip unzip git supervisor \ libmagickwand-dev --no-install-recommends \ && pecl install imagick \ && docker-php-ext-enable imagick \ && docker-php-ext-install pdo_mysql pcntl bcmath RUN pecl install redis \ && docker-php-ext-enable redis RUN mkdir -p /var/log/supervisor COPY supervisord.prod.conf /etc/supervisor/conf.d/supervisord.conf COPY . /var/www RUN chown -R www-data:www-data \ /var/www/storage \ /var/www/bootstrap/cache RUN mkdir -p /tmp/storage/bootstrap/cache |
Molto semplice, in realtà: non fa altro che installare tutte le dipendenze necessarie e, partendo dalla cartella corrente (questo file Dockerfile si trova nella root del mio progetto) copia tutti i contenuti in /var/www, il path nel container in cui l’applicazione si aspetta di essere trovata.
Nota: non fate l’errore di usare questo Dockerfile come copia e incolla. Ogni progetto ha un suo setup specifico, e molto probabilmente questo non è quello che vi serve. Usatelo come traccia.
A questo punto, l’immagine può essere costruita correttamente seguendo una qualsiasi pipeline che aiuti nel lavoro. Io personalmente mi trovo divinamente con le Github actions, alle quali ho affidato tutto il mio workflow.
Parlerò di Github actions nel dettaglio molto presto. Per rimanere sul pezzo e leggere l’articolo appena uscirà, iscriviti alla newsletter usando il menu qui a destra oppure direttamente da questo link.
Torniamo a noi. Per fare un esempio di quello che dicevo sulle pipeline, mi sono configurato un workflow che, al momento del push sul main branch del mio repository:
- installa le dipendenze PHP;
- installa le dipendenze JS;
- avvia la build dell’app JS;
- tramite una comodissima action creo l’immagine docker e la carico sul mio registry privato (qui trovate qualche info in più);
Tutto questo senza dover fare nulla in locale. Per me è una cosa molto notevole, perché mi permette di lavorare (o almeno, sistemare un bug al volo) letteralmente ovunque, a volte anche senza PC (sì, una volta ho fixato un bug dall’ipad, con questo giochetto).
Mi sono creato anche un’immagine per Nginx, che userò successivamente, partendo da questo Dockerfile:
1 2 3 |
FROM nginx:alpine ADD vhost.conf /etc/nginx/conf.d/default.conf |
e questo file di configurazione:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
server { listen 80; index index.php index.html; root /var/www/public; location / { try_files $uri /index.php?$args; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_buffers 16 16k; fastcgi_buffer_size 32k; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } } |
Stessa cosa vista prima anche qui: bisogna effettuare la build e salvare anche questa immagine sul registry, in modo tale da riusarla successivamente.
Le nostre immagini sono pronte. Tocca passare a…
Preparare il Cluster Kubernetes
Il momento fatidico è arrivato. Tocca iniziare a preparare il nostro cluster… come si fa?
Il primo step consisterà nel creare una serie di file .yml che descriveranno la nostra applicazione in Kubernetes. L’obiettivo di questi file sarà spiegare a Kubernetes:
- che variabili d’ambiente usare per fare in modo che la nostra app sappia a quale database connettersi, o quale driver usare per le code;
- quanti pod istanziare per la nostra applicazione web;
- quanti pod istanziare per la parte relativa ai worker, per i vari job in coda;
- altre cose “accessorie”, come i cronjob e l’esecuzione delle migration al momento giusto;
In questo articolo guarderemo più da vicino il setup delle variabili d’ambiente e del servizio web. Nel prossimo articolo, invece, guarderemo più nel dettaglio ciò che riguarda i worker per le code, migration e cronjob.
Prima, però…
Ok, ricominciamo.
Dove li metto questi file?
La prima domanda che ci si fa, quando iniziamo a creare i file .yml da usare, è: “dove li metto?”
Nel mio caso, su Github creo un repository dedicato, che chiamo “app-infra”, dove “app” è il nome dell’applicazione. Ad esempio, per la mia app “Ahia! Easy Price Changer” ho due repository:
- ahia-app, quello dell’applicazione Laravel;
- ahia-infra, quello dove salvo tutti i file di configurazione dell’infrastruttura;
Chiaramente, non è l’unico modo di lavorare. Per sperimentare, ad esempio, può andare benissimo la Dashboard che DigitalOcean mette a disposizione (si può creare tutto tranquillamente anche dall’interfaccia lì sopra). Oppure, si può decidere di usare un tool da linea di comando come kubectl, che una volta configurato ci permette di dialogare agevolmente con il cluster.
Io personalmente voto l’approccio repository + kubectl quando lavoro in locale.
Due parole su Nodi, Deployment e Servizi e Pod
In Kubernetes, ogni cluster è composto da uno o più nodi. Nel nostro caso specifico, un nodo è una droplet che DigitalOcean configura per essere assegnato al cluster che abbiamo creato. Kubernetes quindi si occupa di mettere a disposizione del cluster le risorse dei singoli nodi.
Un Pod è l’unità di lavoro più piccola “deployabile” di Kubernetes. Un pod consiste generalmente in un container, o anche più di un container nel caso in cui abbiano bisogno di condividere risorse.
Tutto quello che noi andremo a configurare nelle prossime righe, alla fine della fiera, consisterà in un insieme di pod, il cui numero può variare in base alla grandezza che noi definiamo.
Il Namespace
Nonostante non sia strettamente necessario, Kubernetes permette di raggruppare determinate risorse in un cluster usando un namespace. Particolarmente comodo se si vuole usare lo stesso cluster per più progetti diversi.
Io preferisco usarlo sempre. Si può creare da linea di comando tramite
1 |
kubectl create namespace mynamespace |
dove mynamespace è il nome del namespace che vogliamo creare. Se preferiamo il versioning per tutto, invece, basta un altro piccolo file yml come questo:
1 2 3 4 |
apiVersion: v1 kind: Namespace metadata: name: nome-del-namespace-qui |
Le Variabili d’Ambiente
Le variabili d’ambiente che useremo vanno messe in un apposito file yml, che viene chiamato ConfigMap nel gergo di Kubernetes.
Eccolo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
apiVersion: v1 kind: ConfigMap metadata: name: laravel-config labels: name: laravel-config namespace: ahia-store data: APP_ENV: "production" APP_KEY: "base64:blablablabla..." APP_DEBUG: "false" DB_PORT: "25060" DB_HOST: "host-del-mio-managed-db.ondigitalocean.com" DB_DATABASE: "nome_database" DB_USERNAME: "nome_utente" DB_PASSWORD: "password_utente" BROADCAST_DRIVER: "pusher" CACHE_DRIVER: "redis" SESSION_DRIVER: "redis" QUEUE_CONNECTION: "redis" # e così via... |
Nel mio caso, come è possibile vedere, ho usato il namespace ahia-store per la mia applicazione (il nome di dominio è ahia.store…).
Nell’elemento data sono andato a mettere tutte le variabili d’ambiente che saranno disponibili per ogni container dell’applicazione.
Una volta creato il file, supponiamo in env.yml, non devo fare altro che eseguire un’operazione di apply per creare questa configurazione nel cluster.
1 |
kubectl apply -f ./env.yml |
Suggerimento: non mettete questo file in versioning, perché contiene dati sensibili! In genere io creo un env.yml.example che ne descrive la struttura.
Giusto qualche secondo e, se non abbiamo sbagliato niente, potremo vedere la nostra config map sul cluster (la Dashboard è perfetta per controllare queste cose).
Il Deployment
Per il deploy dell’immagine dell’applicazione vera e propria, quindi, ho deciso di usare un Deployment. Ho creato un file deployment_web.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
apiVersion: apps/v1 kind: Deployment metadata: name: web spec: # Qui sto indicando che voglio 3 repliche (pod) # per la mia applicazione web. replicas: 3 # La strategia per il rollout dei pod in caso di deploy. # In questo modo, se qualcosa va storto, ne ho comunque # almeno uno sempre funzionante. strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 1 minReadySeconds: 5 selector: matchLabels: app: web template: metadata: labels: app: web spec: # La nota dopo questo snippet spiega di che si tratta. imagePullSecrets: - name: docker.pkg.github.com containers: # Il primo container, quello dell'app. - name: laravel image: docker.pkg.github.com/ORGANIZATION/APP-IMAGE:latest imagePullPolicy: Always ports: - containerPort: 9000 # Le variabili d'ambiente verranno lette dalla config map # creata poco fa. envFrom: - configMapRef: name: laravel-config # La seconda immagine, quella di Nginx creata prima. - name: nginx image: docker.pkg.github.com/ORGANIZATION/NGINX-IMAGE:latest imagePullPolicy: Always ports: - containerPort: 80 |
Nota: quel docker.pkg.github.com in imagePullSecrets fa riferimento ad un secret che ho creato precedentemente nel cluster. Mi serve per poter accedere al mio registry privato su Github.
L’ho creato sul cluster tramite l’istruzione
1 |
kubectl create secret docker-registry docker.pkg.github.com --docker-server=docker.pkg.github.com --docker-username=$DOCKER_USERNAME --docker-password=$DOCKER_PASSWORD --docker-email=$DOCKER_EMAIL --namespace=ahia-store |
dove $DOCKER_USERNAME, $DOCKER_PASSWORD e $DOCKER_EMAIL sono i dati del mio utente.
Non rimane altro che il servizio. Un servizio, in Kubernetes, è un modo più “astratto” che permette di esporre un’applicazione che gira su più pod contemporaneamente.
Questo per un motivo ben preciso: i pod non sono qualcosa di permanente. Possiamo crearli, distruggerli, ridurne il numero oppure aumentarlo in base alle necessità.
Un service, volendola semplificare, permette di “raccogliere in un unico punto” le informazioni su come accedere a tutti i pod presenti, in modo tale da permettere alla nostra app di avere un unico punto d’accesso.
Un servizio è molto meno verboso. Creiamo un file service_web.yml.
1 2 3 4 5 6 7 8 9 10 |
apiVersion: v1 kind: Service metadata: name: web spec: ports: - port: 80 targetPort: 80 selector: app: web |
Quello che viene fatto, qui, è abbastanza semplice: questo service “web” non fa altro che puntare la porta 80 di ogni pod la cui label app corrisponde a web. Kubernetes assegnerà a questo servizio un IP specifico, che potrà essere usato come punto d’accesso.
Ovviamente, sarà proprio Kubernetes a scandire continuamente il cluster per sapere se ci sono nuovi Pod da agganciare al servizio o “vecchi” da rimuovere. Comodo!
Una volta creati questi file, non scordiamoci di eseguire
1 2 |
kubectl apply -f ./deployment_web.yml kubectl apply -f ./service_web.yml |
per applicare le nuove modifiche al cluster.
Per oggi basta!
Stavolta ho parlato un bel po’, ma vi avevo promesso di iniziare a vedere un po’ di Kubernetes e odio non mantenere le promesse. Ammetto che è tanta carne al fuoco, e Kubernetes non è molto semplice. Se qualcosa non è chiaro lasciate pure un commento, risponderò appena possibile!
Nel prossimo ed ultimo articolo di questa mini-serie vedremo nel dettaglio come ho configurato i vari worker per le code, cronjob e migration, insieme a qualche consiglio “finale”. Se questo articolo vi è piaciuto, potete:
- seguirmi su Facebook, sulla pagina Francesco Codes;
- iscrivervi alla newsletter, per ricevere le news di questo blog direttamente via mail, tramite il form qui a lato oppure usando questo link;
Cosa? Non ti basta? Leggi subito la terza parte della serie qui!