Come usare Vite.js con Helm

  • Di
  • 2023-09-14 - 10 minuti
banner

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"]

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

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, come DockerHub 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

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

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

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" . }}
...

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 }}

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 }}
...

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
...

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
...

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 }}
...

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"
...

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
...

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 }}

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 }}

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 }}
...

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

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"

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  

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

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)

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}")

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

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

Post correlati

Iscriviti al TheRedCode.it Corner

La tecnologia corre, e tu devi correre più veloce per rimanere sempre sul pezzo! 🚀

Riceverai una volta al mese (o anche meno) con codici sconto per partecipare agli eventi del settore, quiz per vincere dei gadget e i recap degli articoli più interessanti pubblicati sul blog

Ci sto!

Partners

Community, aziende e persone che supportano attivamente il blog

Logo di Codemotion
Logo di GrUSP
Logo di Python Milano
Logo di Schrodinger Hat
Logo di Python Biella Group
Logo di Fuzzy Brains
Logo di Django Girls
Logo di Improove
Logo del libro open source
Logo di NgRome

Vuoi diventare #tech content writer? 🖊️

Se vuoi raccontare la tua sul mondo #tech con dei post a tema o vuoi condividere la tua esperienza con la community, sei nel posto giusto! 😉

Manda una mail a collaborazioni[at]theredcode.it con la tua proposta e diventa la prossima penna del blog!

Ma sì, facciamolo!