DEV Community

Serena Sensini
Serena Sensini

Posted on • Originally published at theredcode.it on

Come usare Vite.js con Helm

Hai bisogno di sapere come portare la tua applicazione su Kubernetes in Vite.js da zero?

Sei nel posto giusto.

Unico requisito per seguire questa guida: avere un’applicazione Vite.js. Nel caso in cui tu non ne abbia una pronta,qui trovi un esempio di un counter scritto in Vite.js.

Di base, questo esempio non fa altro che esporre una pagina dove è presente un pulsante che riporta il numero di click eseguiti:

Il progetto -al momento- ha questa struttura:

Ma andiamo avanti, e vediamo step-by-step come arrivare ad avere un’istanza su Kubernetes:

Dockerfile

Per far sì che funzioni su Kubernetes, è necessario avere un’immagine: per avere un’immagine, serve un Dockerfile che ci permetta di configurare l’applicazione attraverso dei semplici passaggi:

  • installazione di Node.js 18, dal momento che stiamo utilizzando la versione Vite.js 4.4.5+ (istruzione FROM)
  • specificare la cartella di lavoro dove i file del codice sorgente verranno copiati (istruzione WORKDIR)
  • copiare tutti i file necessari all’installazione delle dipendenze (istruzione COPY)
  • installare npm e le relative dipendenze (istruzione RUN)
  • copiare i restanti file riguardanti l’applicazione (istruzione RUN)
  • specificare la porta 5173, ossia la porta di default utilizzata da Vite.js per esporre l’applicazione (istruzione_EXPOSE_)
  • definire qual è il comando con cui l’applicazione viene avviata (istruzione CMD).
FROM node:18
LABEL authors="TheRedCode.it"

WORKDIR /app

COPY package*.json ./

RUN npm i && npm install -g npm@9.8.1

COPY . .

RUN mkdir /.npm && \
 chgrp -R 0 /app && \
 chmod -R ug+rwX /app && \
 chown -R 1001:0 /app && \
 chgrp -R 0 /.npm && \
 chmod -R ug+rwX /.npm && \
 chown -R 1001:0 /.npm;

EXPOSE 5173

USER 1001

CMD ["npm", "run", "dev"]

Enter fullscreen mode Exit fullscreen mode

Le restanti righe, come l’istruzione RUN che contiene i comandi mkdir e chgrp o quella contenente l’istruzione USER servono per assegnare un utente che non abbia i permessi di amministratore al container e per far sì che possa eseguire i file necessari all’avvio dell’applicazione: questo perché vogliamo che l’immagine sia sicura e segua la best practices in materia di container.

Ognuna delle righe precedenti può e deve essere adattata all’applicazione utilizzata: ad esempio, se la porta utilizzata è diversa da quella riportata, è sufficiente sostituirla; stesso vale per la versione di Node.js utilizzata o il comando che viene specificato per avviare l’applicazione in locale.

A questo punto, testiamo l’immagine con il comando seguente:

docker build -t vite-app .
docker run -d vite-app -p 5173:5173

Enter fullscreen mode Exit fullscreen mode

Anche qui, utilizziamo il parametro -p per specificare la porta dell’applicazione (parametro a destra) da esporre localmente (parametro a sinistra).

Se tutto ha funzionato correttamente, dovremmo ottenere lo stesso risultato visto in precedenza:

Ora che l’immagine è pronta, dobbiamo renderla accessibile tramite un registry : esistono diverse opzioni, comeDockerHub o Quay.io, che richiedono solamente un account gratuito.

Nel caso di DockerHub, è sufficiente creare un account, creare un repository e poi eseguire il login da riga di comando per poter eseguire il tag e il push dell’immagine.

docker login -u IL_MIO_UTENTE

docker tag vite-app IL_MIO_UTENTE/vite-app:0.0.1

docker push IL_MIO_UTENTE/vite-app:0.0.1

Enter fullscreen mode Exit fullscreen mode

Nel caso di Quay.io, è sufficiente creare un account, creare un repository e poi eseguire il login da riga di comando per poter eseguire il push dell’immagine.

docker login quay.io -u IL_MIO_UTENTE

docker tag vite-app IL_MIO_UTENTE/vite-app:0.0.1

docker push IL_MIO_UTENTE/vite-app:0.0.1

Enter fullscreen mode Exit fullscreen mode

Creare il Chart

Abbiamo già visto cos’è Helm e come funziona: vediamo come contestualizzarlo su Vite.js.

Per iniziare a costruire il Chart , utilizziamo la riga di comando ed eseguiamo il comando:

helm create vite-chart
>>>
Creating vite-chart

Enter fullscreen mode Exit fullscreen mode

Questo andrà a creare una cartella con i diversi file necessari alla sua esecuzione:

Ora dobbiamo toglierci il cappello da #dev e passare a quello di #architect per poter analizzare dall’alto l’applicazione e scegliere le giuste risorse Kubernetes necessarie al suo deploy.

Stateless o no?

Partiamo dall’inizio: l’applicazione è stateless? Ossia, è in grado di girare senza la necessità di salvare
alcun dato riguardo la sessione? Nel caso di esempio, : possiamo quindi utilizzare un oggetto come il Deployment per eseguire il container, che è pensato nativamente per applicazioni stateless.

Apriamo dunque il file deployment.yaml nella cartella templates e iniziamo ad analizzarlo -e modificarlo.

Già dalle prime righe, notiamo che ci sono alcune istruzioni che, per chi non avesse familiarità con i Chart, potrebbero risultare complicate: l’istruzione include, ad esempio, fa riferimento a un file chiamato helpers.tpl che rappresenta un template all’interno del quale è possibile includere diverse porzioni di YAML che rappresentano i valori con cui quel campo (nel caso di esempio seguente, name), dovrà essere valorizzato.

apiVersion: apps/v1
kind: Deployment
metadata:
 name: {{ include "vite-chart.fullname" . }}
...

Enter fullscreen mode Exit fullscreen mode

Giusto per avere un riferimento, nel file _helpers.tpl avremo una definizione di questo tipo, dove il campo_fullname_ viene valorizzato troncando a 63 il numero massimo di caratteri per evitare problemi di sintassi con alcune risorse Kubernetes che hanno questa limitazione; il tutto viene fatto tramite alcune istruzioni if/else che controllano diversi aspetti del campo per adattarlo al caso d’uso.

{{- define "vite-chart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

Enter fullscreen mode Exit fullscreen mode

Dal momento che vogliamo semplificarci la vita , faremo uso del solo file values.yaml, mentre il file _helpers. tpl verrà scartato, per cui possiamo rimuovere l’istruzione include dallo YAML che definisce il Deployment e inserire un campo nel file values.yaml per definire il fullname, così da avere un’istruzione che da riutilizzare, in questo modo: aggiungo il campo fullname al file values.yaml, e poi inserisco l’istruzione che lo sostituisce nel file deployment.yaml:

# Default values for vite-chart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
 repository: "ssensini/vite-app"
 pullPolicy: IfNotPresent
 # Overrides the image tag whose default is the chart appVersion.
 tag: "0.0.1"

imagePullSecrets: []
nameOverride: ""
fullname: "vite-chart"
...


apiVersion: apps/v1
kind: Deployment
metadata:
 name: {{ .Values.fullname }}
...

Enter fullscreen mode Exit fullscreen mode

Lo stesso principio può essere applicato per i diversi campi, come replicas e via dicendo, se vogliamo rendere parametrizzabile ogni aspetto del Deployment. In questo caso, proseguiamo con la lettura del file:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: {{ .Values.fullname }}
 labels:
 app: vite-chart
spec:
 {{- if not .Values.autoscaling.enabled }}
 replicas: {{ .Values.replicaCount }}
 {{- end }}
 selector:
 matchLabels:
 app: vite-chart
...

Enter fullscreen mode Exit fullscreen mode

Nel caso di labels e matchLabels, inseriamo una label di default chiamata app:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: {{ .Values.fullname }}
 labels:
 app: vite-chart
...

Enter fullscreen mode Exit fullscreen mode

Andando avanti nella lettura del file, vediamo che viene aggiunto il campo imagePullSecret, serviceAccountName e_securityContext_: questi sono valorizzati come vuoti nel file values.yaml e in effetti al momento non sono necessari, quindi proseguiamo ignorando o rimuovendo questa sezione.

...
 spec:
 {{- with .Values.imagePullSecrets }}
 imagePullSecrets:
 {{- toYaml . | nindent 8 }}
 {{- end }}
 serviceAccountName: {{ include "vite-chart.serviceAccountName" . }}
 securityContext:
 {{- toYaml .Values.podSecurityContext | nindent 8 }}
...

Enter fullscreen mode Exit fullscreen mode

Nella sezione relativa al container che verrà creato, troviamo invece le informazioni sull’immagine da utilizzare: questa fa sempre riferimento al file values.yaml che, di default, viene valorizzata con l’immagine di Nginx in una specifica versione. Sostituiamo con quella presente nel repository creato in precedenza direttamente nel file_values.yaml_:

...
# Default values for vite-chart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image:
 repository: "IL_MIO_UTENTE/vite-app"
 pullPolicy: IfNotPresent
 # Overrides the image tag whose default is the chart appVersion.
 tag: "0.0.1"
...

Enter fullscreen mode Exit fullscreen mode

Continuando, la porta di default è impostata a 80, mentre dovrebbe essere 5173 : sempre nel file nel file_values.yaml_ modifichiamo questo valore:

...
service:
 type: ClusterIP
 port: 5173
...

Enter fullscreen mode Exit fullscreen mode

Al momento, rimuoviamo le sezioni livenessProbe e readinessProbe, dal momento che non sono presenti questa tipologia di controlli nella nostra applicazione e ignoriamo la sezione relativa alle risorse (chiamata resources), che definiscono la CPU e la memoria necessarie all’applicazione per poter funzionare.

Rimuovendo anche gli ultimi campi, il Deployment dovrebbe risultare in questo modo:

apiVersion: apps/v1
kind: Deployment
metadata:
 name: {{ .Values.fullname }}
 labels:
 app: vite-chart
spec:
 {{- if not .Values.autoscaling.enabled }}
 replicas: {{ .Values.replicaCount }}
 {{- end }}
 selector:
 matchLabels:
 app: vite-chart
 template:
 metadata:
 {{- with .Values.podAnnotations }}
 annotations:
 {{- toYaml . | nindent 8 }}
 {{- end }}
 labels:
 {{- include "vite-chart.selectorLabels" . | nindent 8 }}
 spec:
 {{- with .Values.imagePullSecrets }}
 imagePullSecrets:
 {{- toYaml . | nindent 8 }}
 {{- end }}
 serviceAccountName: {{ include "vite-chart.serviceAccountName" . }}
 securityContext:
 {{- toYaml .Values.podSecurityContext | nindent 8 }}
 containers:
 - name: {{ .Chart.Name }}
 securityContext:
 {{- toYaml .Values.securityContext | nindent 12 }}
 image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
 imagePullPolicy: {{ .Values.image.pullPolicy }}
 ports:
 - name: http
 containerPort: {{ .Values.service.port }}
 protocol: TCP
 resources:
 {{- toYaml .Values.resources | nindent 12 }}

Enter fullscreen mode Exit fullscreen mode

Comunicare tramite Service

Per poter permettere l’accesso alla rete del cluster, è necessario utilizzare una risorsa di tipo Service : durante la creazione del Chart, è stato infatti creato un file service.yaml che ne definisce uno standard. Analizziamolo:

apiVersion: v1
kind: Service
metadata:
 name: {{ include "vite-chart.fullname" . }}
 labels:
 {{- include "vite-chart.labels" . | nindent 4 }}
spec:
 type: {{ .Values.service.type }}
 ports:
 - port: {{ .Values.service.port }}
 targetPort: http
 protocol: TCP
 name: http
 selector:
 {{- include "vite-chart.selectorLabels" . | nindent 4 }}

Enter fullscreen mode Exit fullscreen mode

Come fatto in precedenza, sostituiamo il valore del campo name con quello recuperato dal file values.yaml:

apiVersion: v1
kind: Service
metadata:
 name: {{ .Values.fullname }}
...

Enter fullscreen mode Exit fullscreen mode

Per il resto, il file riporta i valori del file values.yaml correttamente definendo un Service di tipo ClusterIP esponendo la porta 5173 tramite protocollo TCP e chiama la porta del Service http.

apiVersion: v1
kind: Service
metadata:
 name: {{ .Values.fullname }}
 labels:
 app: vite-chart
spec:
 type: {{ .Values.service.type }}
 ports:
 - port: {{ .Values.service.port }}
 targetPort: {{ .Values.service.port }}
 name: http
 protocol: TCP

Enter fullscreen mode Exit fullscreen mode

In questo modo, l’applicazione sarà in grado di comunicare con altri Pod all’interno del cluster.

Ultimo step: il file Chart.yaml: verifichiamo che le informazioni riguardo i metadati del Chart che andremo a creare siano corrette, rimuoviamo eventuali commenti e proseguiamo.

apiVersion: v2
name: vite-chart
description: A Helm chart for Kubernetes

type: application
version: 0.1.0

appVersion: "1.16.0"

Enter fullscreen mode Exit fullscreen mode

Ultimi ritocchi

Rimuoviamo una serie di file che non ci servono: in particolare, rimuoviamo hpa.yaml e serviceaccount.yaml e controlliamo che la sintassi di quanto fatto finora sia corretta tramite il comando helm lint: questo andrà a verificare se i file finora modificati contengono errori o meno.

helm lint .
==> Linting .
[INFO] Chart.yaml: icon is recommended

1 chart(s) linted, 0 chart(s) failed

Enter fullscreen mode Exit fullscreen mode

Perfetto, tutto funziona perfettamente!

Installazione

A questo punto, possiamo testare il Chart all’interno di un cluster Kubernetes. Per farlo, dopo esserci collegati, possiamo eseguire il comando helm install sfruttando il file values.yaml e assegnando vite-app come nome alla release, in questo modo:

helm install -f values.yaml vite-app ./
>>>
NAME: vite-app
LAST DEPLOYED: ...
NAMESPACE: kube-public
STATUS: deployed
REVISION: 1
NOTES:
1. Get the application URL by running these commands:
 export POD_NAME=$(kubectl get pods --namespace kube-public -l "app.kubernetes.io/name=vite-chart,app.kubernetes.io/instance=vite-app" -o jsonpath=
"{.items[0].metadata.name}")
 export CONTAINER_PORT=$(kubectl get pod --namespace clinalytix $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
 echo "Visit http://127.0.0.1:8080 to use your application"
 kubectl --namespace clinalytix port-forward $POD_NAME 8080:$CONTAINER_PORT

Enter fullscreen mode Exit fullscreen mode

Esporre l’applicazione

Ok, sembra che il pod stia funzionando: e ora come vi accedo? Per fare un test al volo, possiamo sfruttare i comandi riportati nell’output e modificarli per nostra comodità.

In primis, eseguiamo il comando di export che ci permette di recuperare il nome del pod dell’applicazione utilizzando la label app e fornendo come output il solo campo name:

export POD_NAME=$(kubectl get pods --namespace kube-public -l "app" -o name)

Enter fullscreen mode Exit fullscreen mode

Poi, recuperiamo la porta del container esposta dal pod con il secondo comando, sostituendo il nome del Pod in questo modo:

export CONTAINER_PORT=$(kubectl get $POD_NAME --namespace kube-public -o jsonpath="{.spec.containers[0].ports[0].containerPort}")

Enter fullscreen mode Exit fullscreen mode

Infine, eseguiamo il _ port-forward _ della porta esposta dal container (ricordiamo, la 5173) verso la porta 8080 locale (o un’altra qualsiasi porta):

kubectl --namespace kube-public port-forward $POD_NAME 8080:$CONTAINER_PORT

Enter fullscreen mode Exit fullscreen mode

Fatto questo, ci basterà aprire il browser e digitare http://localhost:8080 per visualizzare la pagina principale dell’applicazione:

In realtà, per esporre l’applicazione ci sono diversi altri modi, a seconda del tipo di configurazione di rete che è supportata: è possibile creare un LoadBalancer , oppure sfruttare un Ingress

Ma questo lo vedremo in un’altra puntata!

Risorse utili

Top comments (0)