Files
Tesi-Magistrale/readme.md
Lorenzo Venerandi 88760a8a8f updated readme
2025-04-01 19:54:09 +02:00

916 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

L'obiettivo di questo elaborato è progettare ed implementare una
soluzione che consenta la distribuzione di applicazioni nel panorama
dell'Edge-Cloud continuum. Le applicazioni devono essere sviluppate
seguendo il paradigma FaaS ed essere in grado di funzionare
indipendentemente dall'infrastruttura sottostante e senza essere
ri-compilate. Inoltre, la piattaforma deve essere in grado di effettuare
migrazioni live delle applicazioni e spostare il carico computazionale
in modo automatico in risposta a problemi infrastrutturali o a necessità
di bilanciamento del carico. La migrazione deve avvenire anche fra
ambienti eterogenei, per esempio deve essere possibile spostare
l'esecuzione di un'applicazione da un nodo Edge ad uno Cloud.\
Le applicazioni in esecuzione devono poter interagire e scambiarsi
informazioni, quindi c'è necessità di utilizzare un sistema di
messaggistica distribuito che si sposi con il modello di esecuzione ad
eventi del paradigma FaaS.\
Per facilitare l'adozione del paradigma FaaS è stato scelto un modello
applicativo basato sui microservizi, in
particolare facente uso della tecnologia Wasm. La scelta è stata fatta
in quanto riesce a superare una criticità associata alla tradizionale
soluzione del container. Essi infatti vengono eseguiti in runtime
strettamente collegati al kernel del sistema operativo in cui risiedono
ed esso cambia con l'architettura della macchina. Questo costringerebbe
ad avere una versione dell'applicazione per ogni architettura
supporata.\
Le applicazioni compilate in Wasm invece possono operare in qualsiasi
host senza preoccuparsi di requisiti infrastrutturali, consentendo
l'utilizzo dello stesso artifact per ogni host.
![Modello Edge Computing](res/img/schemi-Scenari.drawio.svg)
Per soddisfare i requisiti di migrazione e bilanciamento automatico fra
nodi si è scelto di adottare wasmCloud, principalmente per i seguenti
motivi:
- Supporto all'esecuzione di componenti Wasm e specifica WASI Preview
2.
- Supporto sia di ambienti Cloud Native (come Kubernetes o Docker) che
tradizionali (VM con Linux).
- Networking basato su rete mesh Lattice che consente di astrarre
l'infrastruttura sottostante e permette l'interazione dei
componenti.
- Cluster auto-rigenerante e facilmente scalabile all'interno del
Lattice.
- Comunicazione efficiente grazie al backend basato su NATS.
- Gestione delle applicazioni tramite OCI Registry.
NATS è stata la soluzione selezionata anche per quanto riguarda la
comunicazione e l'interazione delle applicazioni stesse, principalmente
per il suo supporto alla clusterizzazione e alla distribuzione su
ambienti edge basata su nodi Leaf.
![Infrastruttura
target](res/img/schemi-architettura-infra.drawio.svg)
Il processo di trasformazione delle funzioni in componenti Wasm e la
loro distribuzione verrà approfondita nella prossima sezione.
## PELATO Framework
Lo scopo ultimo di questo elaborato è quindi trasformare una semplice
funzione in un codice compilabile in componente Wasm, configurarlo in
modo che possa essere deployato su ambienti wasmCloud e distribuirlo
sull'infrastruttura.\
La soluzione proposta è un framework denominato PELATO, acronimo di
**Progettazione ed Esecuzione di Lifecycle Automatizzati e Tecnologie
d'Orchestrazione per moduli WebAssembly**, che ha lo scopo di:
- Fornire un modo intuitivo per istanziare un modulo FaaS, quindi
ridurre al minimo le responsabilità dell'utilizzatore che dovrà
esclusivamente specificare il codice della funzione da eseguire
nella task remota.
- Consentire all'utilizzatore di configurare in modo intuitivo i
metadati delle task, come il nome, il target di deployment, ma anche
l'origine e la destinazione dei dati.
- Astrarre i più possibile il processo di generazione del codice e di
Build del componente Wasm.
- Gestire il deployment dei componenti nell'infrastruttura.
Nelle seguenti sezioni verranno approfondite le scelte tecnologiche ed
architetturali che sono state compiute durante la realizzazione di
questo progetto.
### Tecnologie
In questa sezione verranno elencate e spiegate le tecnologie utilizzate
per lo sviluppo di questa soluzione. Ci concentreremo principalmente
sui linguaggi e i tool utilizzati dal framework PELATO, come i linguaggi
Python e Go.
#### Python
Il framework è stato implementato utilizzando Python, un linguaggio di
programmazione ad alto livello interpretato ed orientato agli oggetti,
apprezzato per la sua sintassi chiara e la tipizzazione dinamica che ne
facilitano l'apprendimento e la manutenzione. Grazie a un vasto
ecosistema di librerie è ampiamente utilizzato in ambiti come data
science, sviluppo web e intelligenza artificiale. Supporta diversi
paradigmi di programmazione e, grazie alla gestione automatica della
memoria, riduce la complessità nello sviluppo. Inoltre, la sua
portabilità su Windows, macOS e Linux lo rende adatto a molteplici
contesti, dalla prototipazione rapida alle soluzioni enterprise.\
È noto che Python, pur offrendo un ecosistema ricco e una sintassi
intuitiva, può presentare limitazioni in termini di performance,
soprattutto per operazioni CPU-intensive. Questo è in gran parte dovuto
al Global Interpreter Lock (GIL), che impedisce l'effettivo sfruttamento
del multi-threading in scenari di calcolo parallelo. Questi problemi
sono stati risolti spostando il carico computazionale ed il parallelismo
su strumenti esterni come Docker (la metodologia verrà spiegata in
seguito).\
La scelta di impiegare questo linguaggio per sviluppare il framework è
stata motivata principalmente dalla vasta gamma di librerie disponibili.
In particolare, per lo sviluppo di PELATO ce ne sono due fondamentali:
- **Jinja**: è un motore di template per Python che permette di
generare contenuti dinamici combinando testo statico e logica di
programmazione. Utilizzato in framework web come Flask e strumenti
di automazione come Ansible, trova applicazione anche nella
configurazione di orchestratori come Helm, dove consente di
parametrizzare file YAML. Attraverso la sintassi con doppie
parentesi graffe, Jinja sostituisce variabili con valori definiti,
rendendo la configurazione più flessibile e riutilizzabile. È stato
utilizzato in fase di generazione del codice per parametrizzare lo
stesso in base alla configurazione specificata dall'utente.
- **docker-py**: questa libreria consente di interagire con le API di
Docker direttamente da Python, facilitando la gestione e
l'automazione dei container. Permette operazioni come la creazione,
l'avvio e l'eliminazione di container, oltre alla gestione di
immagini e volumi. Questa integrazione è stata fondamentale per
poter parallelizzare le operazioni di build e di deployment dei
moduli Wasm.
#### Golang
Un altro linguaggio utilizzato è Go, noto anche come Golang, un progetto
open source sviluppato da Google per offrire un equilibrio tra
efficienza, sicurezza e semplicità. Grazie alla tipizzazione statica, al
garbage collector e a un avanzato sistema di concorrenza basato sulle
goroutine, Go consente la creazione di applicazioni scalabili e
performanti. Pur supportando la programmazione orientata agli oggetti
attraverso interfacce e struct, mantiene una sintassi essenziale e
pragmatica. Inoltre, la sua ricca standard library fornisce strumenti
per la gestione della rete, il parsing di file e la sicurezza
crittografica.\
Go è il linguaggio selezionato per la programmazione delle funzioni da
eseguire nei componenti Wasm: l'utente che utilizza il framework PELATO
dovrà programmare i propri flussi utilizzando Go.\
La scelta di utilizzare questo linguaggio è stata effettuata
principalmente perché è uno dei due linguaggi (insieme a Rust) che
attualmente implementa la specifica WASI Preview 0.2, necessaria per la
composizione delle applicazioni tramite components e providers su
wasmCloud.\
La scelta di Go invece di Rust è avvenuta perché la sintassi di
quest'ultimo risulta complessa e di difficile lettura, rendendolo meno
accessibile per utenti senza esperienza pregressa. Al contrario, Go
offre una sintassi più chiara e intuitiva, facilitando lo sviluppo e la
manutenzione del codice.
### Struttura del framework
In questa sezione verrà data una descrizione architetturale ad alto
livello di PELATO. Il framework è sviluppato secondo un'architettura che
adotta il pattern di progettazione `Facade` che fornisce un'interfaccia
semplificata e unificata per accedere alle varie funzionalità. In
pratica, una classe Facade funge da punto di accesso centrale,
nascondendo la complessità interna e offrendo metodi di alto livello per
interagire con il sistema.\
La classe `Pelato` viene utilizzata come punto di accesso per la CLI
(Command Line Interface), memorizza le configurazioni iniziali e si
occupa di salvare le metriche. Inoltre è responsabile dell'esecuzione
della logica di business implementata nei tre package:
- `code_generator`
- `wasm_builder`
- `component_deploy`
La configurazione iniziale della classe avviene tramite variabili
d'ambiente (che possono essere caricate dal framework tramite un file
`.env` locale, verranno dati più dettagli in seguito); ciò consente una
facile implementazione del framework come applicazione containerizzata,
nel caso si volesse "trasformare\" in un servizio gestito in Cloud.\
Questo pattern di progettazione aumenta la modularità dei componenti
facilitando eventuali estensioni dove si fa l'esempio di un API
Server affiancato alla CLI.
![Architettura framework
PELATO](res/img/schemi-implementazione-struttura.drawio.svg)
## Configurazione ed esecuzione framework
In questa sezione verrà approfondita la configurazione del framework.
Come già detto in precedenza il codice è scritto in Python, quindi sarà
necessario avere un interprete di Python3 installato (consigliato
`Python 3.12`), inoltre sarà necessario installare le librerie
specificate nel `requirement.txt`. L'approccio consigliato è quello di
utilizzare un virtual environment, cioè un'istanza dell'interprete
Python che consente di installare le librerie localmente, senza toccare
l'interprete configurato nel sistema o preoccuparsi di eventuali
incompatibilità.\
La configurazione dinamica avviene tramite variabili d'ambiente,
ottenute dal sistema all'inizio di ogni esecuzione. Per facilitare il
processo di configurazione è possibile creare un file `.env` situato
nella cartella del framework: all'inizio di ogni esecuzione il file .env
viene parsato e le variabili al suo interno vengono utilizzate per
impostare il servizio.\
All'interno della repository è predisposto il file `.env.template`, nel
quale sono riportate le variabili d'ambienti impostabili e la
configurazione di default, che viene mostrata di seguito.
```sh
REGISTRY_URL=
REGISTRY_USER=
REGISTRY_PASSWORD=
PARALLEL_BUILD=True
NATS_HOST=localhost
NATS_PORT=4222
ENABLE_METRICS=True
```
Andiamo ad analizzarle:
- `REGISTRY_URL`, `REGISTRY_USER`, `REGISTRY_PASSWORD` servono per
impostare le informazioni del registry in cui verranno caricate le
immagini OCI dei moduli Wasm.
- `PARALLEL_BUILD` abilita l'esecuzione dei container in modalità
parallela.
- `NATS_HOST` e `NATS_PORT` sono utilizzati in fase di deployment e
rappresentano l'istanza di NATS a cui il framework si collega per
deployare le applicazioni sul cluster wasmCloud.
- `ENABLE_METRICS` abilita la memorizzazione delle metriche ad ogni
esecuzione del codice.
### Setup progetto
L'utente finale, una volta configurato l'ambiente di esecuzione e
impostate le variabili, per poter utilizzare il framework dovrà creare
un progetto contenente:
- Cartella `task` contenente i file Go in cui vengono definite le
funzioni eseguite dai componenti Wasm.
- File `workflow.yaml` in cui inserire la configurazioni dei vari Task
(per esempio nome, versione, template e nome del file Go) e sarà
utilizzato dal generatore per compilare i template Jinja. Un esempio
di file workflow viene mostrato di seguito.
Le modalità di compilazione di questi file verranno approfondite nel
capitolo dedicato alla generazione del codice.
```yaml
project_name: Test_project
tasks:
- name: Temp sensor read
type: producer_nats
code: sensor_read.go
targets:
- cloud
- edge
source_topic: test_source_data
dest_topic: test_dest_data
component_name: temp_sensor_data
version: 1.0.0
...
```
### PELATO CLI
Come già anticipato in precedenza l'interfacciamento fra utente e
framework è realizzata tramite una CLI, cioè un'interfaccia a riga di
comando in grado di ricevere istruzioni e configurazioni.\
Per invocare la CLI è sufficiente lanciare il comando
`python3 pelato.py`, che restituirà come output le varie opzioni e gli
argomenti necessari, come mostrato nel codice.
``` {#code:pelato_cli caption="Output Pelato CLI" captionpos="b" label="code:pelato_cli"}
usage: pelato.py [-h] {gen,build,deploy,remove,brush} ...
Generate, build and deploy WASM components written in go
Command list
gen Generate Go code
build Build WASM component
deploy Deploy WASM components
remove Remove deployed WASM components
brush Starts the pipeline: gen -> build -> deploy
options:
-h, --help show this help message and exit
```
Ognuno di essi necessita come ulteriore argomento la path della cartella
in cui sono contenuti il file `workflow.yaml` e le `task`. Nel codice vengono riportati alcuni esempi di utilizzo.
```sh
$ python3 pelato.py -h # help
$ python3 pelato.py gen /home/lore/documents/project/ # generazione
$ python3 pelato.py remove project/ # rimozione
$ python3 pelato.py brush project/ # esecuzione pipeline
```
## Pipeline di esecuzione
In questa sezione verrà descritta l'intera pipeline di esecuzione del
framework, basandosi sui passaggi riportati seguente figura.
![Pipeline di esecuzione
PELATO](res/img/schemi-arch-pipeline.drawio.svg)
Adesso verranno analizzati i tre componenti di esecuzione di PELATO:
1. **Generazione**: in questa fase viene parsato il file
`workflow.yaml` e poi utilizzato per generare i progetti Go
necessari per buildare i moduli Wasm. Le configurazioni del file
vengono utilizzate per selezionare il template di base, per
sostituire i valori del template tramite Jinja e per identificare il
file Go contenente la task..
2. **Build**: questa fase si occupa di utilizzare i progetti Go
generati in fase 1 e compilarli per ottenere un componente Wasm,
quindi pubblicarlo come OCI artifact sul registry configurato.
Queste operazioni avvengono all'interno di un container Docker nel
quale sono installati tutti i tool necessari per l'operazione, come
Go, TinyGo, Rust e wash.
3. **Deploy**: è l'ultima fase della pipeline di PELATO, utilizza il
manifest `wadm.yaml` generato in fase 1 per creare l'applicazione
sulla piattaforma wasmCloud. Anche questa operazione avviene
all'interno di un container in quanto necessita del tool wash.
Una volta ultimato il processo, le applicazioni saranno disponibili sul
wasmCloud, o lo diventeranno quando sarà presente un nodo healthy con
label `host-type` corrispondente a quella selezionata dalla task.\
La pipeline del framework è suddivisa nelle tre operazioni di
generazione, build e deployment in modo che ognuna possa funzionare in
modo autonomo (ovviamente se provviste delle risorse necessarie
all'operazione). Inoltre, l'utilizzo di un container per le operazioni
di build e deployment mira ad aumentare la compatibilità e la
distribuzione del framework in modo che non sia dipendente dal sistema
in cui verrà implementato: in questo progetto l'interfaccia è stata
realizzata come CLI utilizzando Python, ma potrebbe essere anche esteso
ad una configurazione SaaS completamente in Cloud o con approccio GitOps
tramite actions e frameworks di CI/CD.\
Tutte e tre le operazioni verranno approfondite nel dettaglio nei
prossimi capitoli.
# Generazione
In questo capitolo verrà approfondito il processo di generazione codice
del progetto Go e del manifest wadm, che verranno impiegati poi per le
fasi di Build e Deploy. Il primo passo è definire con precisione le
struttura e la composizione dei file che fanno parte del progetto
iniziale, quello che dovrà poi configurare l'utente finale che
utilizzerà il framework, strutturato nel modo seguente:
```
├─ workflow.yaml
├─ tasks/
   ├─ task1.go
└─ ...
└─ gen/
```
## Definizione Tasks
In questa sezione ci si concentrerà sulle specifiche dei file task, cioè
quelli situati nella cartella `tasks` del progetto e contenenti le
funzioni che verranno eseguite dai moduli Wasm. Iniziamo fornendo la
struttura base di ogni task file, mostrata nel codice.
``` go
package main
import (
...
)
...
func exec_task(arg string) string{
return ...
}
```
I requisiti da rispettare per la definizione della task sono:
- Il file deve avere un'estensione `.go`.
- Il package deve essere impostato su main (stesso package del main
fornito dal template).
- Funzione di interfacciamento (cioè chiamata dal main) nominata
`exec_task`.
- La funzione di interfacciamento deve accettare una stringa e
restituire una stringa.
La funzione interfaccia accetta e restituisce una stringa per facilitare
il più possibile l'utilizzo da parte di un utente finale, che non dovrà
preoccuparsi di tradurre i tipi Go con i tipi Wasm specificati in WASI
(cosa che viene gestita in "background" sul file main del progetto Go).\
Una volta soddisfatti questi requisiti è possibile eseguire innumerevoli
operazioni, come importare librerie, definire strutture e altre
funzioni. Un esempio è mostrato nel codice seguente, in cui viene importata la libreria `encoding/json` e definita una struct `Request`.
```go
package main
import (
"encoding/json"
)
type Request struct {
Data int
Name string
}
func exec_task(arg string) string{
// unmarshal the data
req := Request{}
json.Unmarshal([]byte(arg), &req)
// do some operations
...
// return the json string
json, _ := json.Marshal(req)
return string(json)
}
```
## Configurazione Workflow
Passiamo ora alla codifica del file `workflow.yaml`, nel quale verranno
effettivamente impostati i task e il loro comportamento.
### Specifica file workflow
Il file deve essere correttamente formattato in Yaml e contenere i
seguenti campi:
- `project_name`: stringa contenente il nome del progetto, può
contenere spazi e viene utilizzato principalmente nella CLI e nelle
metriche.
- `tasks`: lista contenente i vari componenti Wasm.
Andando nel dettaglio, ogni elemento della lista `tasks` deve contenere:
- `name`: stringa contenente il nome del componente, può contenere
spazi e verrà impostato come nome dell'applicazione su wasmCloud.
- `type`: stringa contenente il template da utilizzare come base.
- `code`: stringa contenente il nome del file task da associare al
componente Wasm in fase di generazione.
- `targets`: lista contenente delle stringhe che rappresentano la
label `host-type` associata ai nodi target di deployment dei
componenti Wasm.
- `source_topic`: stringa contenente il topic da cui verranno ricevuti
i dati.
- `dest_topic`: stringa contenente il topic a cui verranno inviati.
- `component_name`: stringa contenente il nome sintetico del
componente, non può contenere spazi e funge da nome per l'OCI
artifact associato.
- `version`: versione del componente, specificata nel formato `x.x.x`.
Insieme al component_name conferisce il nome all'OCI artifact.
### Template
I template sono dei progetti già sviluppati che forniscono certe
funzionalità e fungono da base per il codice fornito dall'utente.
Attualmente sono implementati due template:
- `processor_nats`: comunicazione interamente basata su NATS: i dati
arrivano da un topic e vengono inviati ad un topic.
- `http_producer_nats`: i dati possono provenire sia da un topic NATS
che da una richiesta POST effettuata al nodo in cui è deployata
l'applicazione. Viene utilizzato infatti l'http provider per esporre
un web server nella porta 8000.
Questo approccio consente una facile estendibilità del progetto: infatti
potranno essere sviluppati ed aggiunti nuovi template per aumentare le
funzionalità disponibili sul framework. Un altro template attualmente in
sviluppo è quello per la scrittura dei dati su un DB relazionale.\
Per comprendere meglio le modalità di utilizzo del file workflow
analizziamo un esempio.
```yaml
project_name: Temperature data analysis
tasks:
- name: Temp data conversion
type: http_producer_nats
code: convert.go
targets:
- edge
source_topic: living_room_celsius_data
dest_topic: living_room_kelvin_data
component_name: celsius_to_kelvin_conversion
version: 1.0.0
- name: Data filter
type: processor_nats
code: filter.go
targets:
- cloud
source_topic: living_room_kelvin_data
dest_topic: filtered_kelvin_data
component_name: temp_filter
version: 1.0.2
```
Nel file workflow riportato nel precedente codice vengono definiti due componenti:
- il primo si occupa di ricevere dati inviati da sensori ad un server
http, convertire le temperature e pubblicarli in un topic NATS
- il secondo filtra i risultati, magari rimuovendo errori di
misurazione o aggregandoli
Questo semplice esempio vuole mostrare come con una configurazione
ridotta sia possibile generare, compilare e distribuire un'applicazione
che supporta casi d'uso applicabili al mondo IoT.
## Generazione codice
Il codice che si occupa della generazione è situato sul package
`code_generator` ed è distribuito nei file `generator.py` e
`template_compiler.py`. All'interno del package sono presenti anche i
template utilizzati come base per i progetti Go, situati nella cartella
`templates`. `code_generator/`
### Parsing Workflow
Verrà ora analizzato nel dettaglio il processo di generazione del
codice, partendo dalla fase di analisi del progetto fornito dall'utente.
La prima operazione viene effettuata da `generator.py`, di cui
riportiamo un estratto contenente il codice più rilevante.
```go
def generate(project_dir, registry_url, metrics, metrics_enabled):
...
# Parsing del file workflow.yaml
config = __parse_yaml(f"{project_dir}/workflow.yaml")
# Pulizia della cartella di output
__remove_dir_if_exists(output_dir)
os.makedirs(output_dir, exist_ok=True)
# Per ogni task all'interno del file workflow
for task in config['tasks']:
...
# Compilazione dei template
template_compiler.handle_task(task, output_dir)
# Copia del file task all'interno della cartella di output
shutil.copy2(f"{project_dir}/tasks/{task['code']}", f"{output_dir}/{task['component_name']}/{task['code']}")
if metrics_enabled:
gen_metrics['gen_time'] = '%.3f'%(end_time - start_time)
metrics['code_gen'] = gen_metrics
```
Come si può notare dal Listing
[\[code:code_gen\]](#code:code_gen){reference-type="ref"
reference="code:code_gen"} la funzione `generate` si occupa di parsare
il file workflow, controllarne la validità e preparare la cartella di
output.
### Compilazione template
La generazione vera e propria del codice viene affidata a
`template_compiler` tramite la funzione `handle_task`, la quale
seleziona il template corretto in base a quello riportato nella
configurazione, lo copia nella cartella di output e lo compila
utilizzando Jinja.\
Nelle seguenti porzioni di codice viene riportato un esempio di file (in
questo caso una porzione di `wadm.yaml`) prima e dopo la compilazione
del template tramite Jinja.
```yaml
spec:
components:
- name: {{ component_name }}
type: component
properties:
image: "{{ registry_url }}/{{ component_name }}:{{ version }}"
traits:
- type: link
properties:
target: nats-processor
namespace: wasmcloud
package: messaging
interfaces: [consumer]
- type: spreadscaler
properties:
instances: 1
spread:
{%- set weight = (100 / (targets | length)) | int -%}
{% for target in targets %}
- name: {{ target }}
weight: {{ weight }}
requirements:
host-type: {{ target }}
{% endfor %}
```
$\downarrow$
```yaml
spec:
components:
- name: data_double_test1
type: component
properties:
image: "gitea.rebus.ninja/lore/data_double_test1:1.0.0"
traits:
- type: link
properties:
target: nats-processor
namespace: wasmcloud
package: messaging
interfaces: [consumer]
- type: spreadscaler
properties:
instances: 1
spread:
- name: cloud
weight: 100
requirements:
host-type: cloud
```
A questo punto viene copiato il file specificato sulla configurazione
dalla cartella `task/` del progetto alla cartella di output. La funzione
viene automaticamente agganciata all'handler specifico del file main del
template (dato che appartengono allo stesso package).\
L'intero processo di generazione è stato schematizzato nella seguente figura:
![Processo generazione del
codice](res/img/schemi-implementazione-gen.drawio.svg)
# Build
In questo capitolo verrà mostrato il processo che porta alla
compilazione del processo Go in un componente Wasm.\
Se la fase di generazione è avvenuta con successo, all'interno del
progetto dovrebbe essere presente una cartella chiamata `gen`,
contenente diverse sotto-cartelle con il codice necessario per compilare
i moduli Wasm.
## Wasm Builder
A questo punto può essere invocato il componente `build`, che
sostanzialmente esegue le seguenti operazioni:
1. Istanzia il client Docker utilizzando l'apposito SDK[^2].
2. Controlla se l'immagine `wash-build-image:latest` è presente. Se non
lo è procede a buildarla utilizzando il dockerfile configurato.
3. Per ogni cartella presente dentro `gen` istanzia un container con
`wash-build-image` come immagine e monta la cartella all'interno del
container.
4. Attende la terminazione dei container.
Si può notare come in questo caso le operazioni svolte dal framework
siano limitate: la logica di build del componente e la pubblicazione
dell'artifact OCI sono infatti delegate alle istanze in esecuzione su
Docker.
### Wash build image
Approfondiamo ora il meccanismo utilizzato per compilare i componenti
Wasm, iniziando dal Dockerfile che descrive l'immagine di base
utilizzata, mostrato nel seguente codice.
```dockerfile
FROM ubuntu:24.04 AS wash-build-image
# Install dependencies and tools
RUN apt-get update && apt-get install -y curl wget tar ...
# ----------------- Install WasmCloud -----------------
RUN curl -s "https://packagecloud.io/install/repositories/wasmcloud/core/script.deb.sh" | bash && \
apt-get install -y wash
# ----------------- Install Go 1.23 -----------------
RUN wget https://go.dev/dl/go1.23.4.linux-amd64.tar.gz && \
tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz && \
rm go1.23.4.linux-amd64.tar.gz
# Set Go environment variables
ENV PATH="/usr/local/go/bin:${PATH}"
ENV GOPATH="/go"
ENV GOROOT="/usr/local/go"
# ----------------- Install TinyGo 0.34.0 -----------------
RUN wget https://github.com/tinygo-org/tinygo/releases/download/v0.34.0/tinygo_0.34.0_amd64.deb && \
dpkg -i tinygo_0.34.0_amd64.deb && \
rm tinygo_0.34.0_amd64.deb
# ----------------- Install Rust -----------------
# Install Rust
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y && \
. "$HOME/.cargo/env" && \
cargo install --locked wasm-tools
# Set Rust environment variables
ENV PATH="/root/.cargo/bin:${PATH}"
# Verify installations
RUN go version && tinygo version && cargo --version && wash --version && wasm-tools --version
# ----------------- Build the WasmCloud module -----------------
FROM wash-build-image
RUN mkdir /app
WORKDIR /app
# Install go dependencies, build the wasm module, push it to the registry
CMD ["sh", "-c", "go env -w GOFLAGS=-buildvcs=false && go mod download && go mod verify && wash build && wash push $REGISTRY build/*.wasm && chown -R ${HOST_UID}:${HOST_GID} ."]
```
Il Dockerfile è strutturato in due fasi:
1. **Fase 1**: vengono installate le dipendenze di Go, TinyGo, Rust e
la shell di wasmCloud.
2. **Fase 2**: viene predisposta l'immagine per la compilazione dei
moduli Wasm.
La preparazione della compilazione avviene dentro l'istruzione CMD, che
infatti contiene:
- Istruzioni per risolvere le dipendenze di go ed installarle nel
progetto. In questo modo l'utente può aggiungere librerie supportate
e il builder si occuperà di installarle nel progetto.
- Comando `wash build` che esegue la compilazione del progetto e
genera il componente Wasm all'interno della cartella `gen`.
- Comando `wash push` che pubblica il componente Wasm come artifact
OCI sul registry passato come configurazione.
- Istruzione per impostare i permessi sui file generati, necessario
per poter gestire correttamente i files tramite Python.
### Istanziamento container
Questo approccio consente di utilizzare una sola immagine per tutte le
operazioni di build: le configurazioni dinamiche avvengono tramite
variabili d'ambiente e i file da buildare vengono montati come volume al
posto della cartella `/app`, come possiamo notare dal seguente codice contenente un estratto di codice del componente `build`:
```go
def __build_wasm(task_dir, client, reg_user, reg_pass, detached, wait_list):
oci_url = wadm['spec']['components'][0]['properties']['image']
name = wadm['spec']['components'][0]['name'] + '-build'
...
# Build componente Wasm
print(f" - Building WASM module {oci_url}")
container = client.containers.run(
"wash-build-image:latest",
environment=[f'REGISTRY={oci_url}',
f'WASH_REG_USER={reg_user}',
f'WASH_REG_PASSWORD={reg_pass}',
f'HOST_UID={uid}',
f'HOST_GID={gid}'],
volumes={os.path.abspath(task_dir): {'bind': '/app', 'mode': 'rw'}},
remove = True,
detach = True,
name = name
)
# Build sequenziale o parallela
if detached == 'False':
container.wait()
else:
wait_list.append(name)
```
### Esecuzione parallelizzata
Il processo di build può essere eseguito in modalità sequenziale o
parallela, a seconda della configurazione delle variabili d'ambiente.
Questo comportamento è gestito tramite la flag `detach`, che permette di
avviare i container in modo asincrono. In questa modalità, viene
utilizzata una waiting list per istanziare tutti i container in
parallelo e attendere il completamento dell'operazione di build.\
## Processo completo
L'intero processo di build viene mostrato nella seguente figura:
![Processo di Build componente
Wasm](res/img/schemi-implementazione-build.drawio.svg)
# Deployment
In questo capitolo verrà descritta la procedura di deployment dei
componenti Wasm nella piattaforma wasmCloud. La comunicazione fra questa
ed il framework PELATO avviene tramite NATS: sarà sufficiente
configurare il framework con le credenziali di un client NATS collegato
al cluster per poter deployare le applicazioni.\
Anche in questo caso il componente deploy si appoggia a Docker per
l'operazione, dato che l'applicazione del deployment tramite il manifest
`wadm.yaml` ottenuto in fase di Generazione deve essere effettuata
utilizzando `wash`.
## Application Deployment
La fase di deployment è strutturata in modo molto simile a quella di
build, infatti le operazioni svolte dal componente deploy sono:
1. Istanziamento del client Docker.
2. Controllo dell'immagine `wash-deploy-image:latest`, se non è
presente procede a buildarla utilizzando il dockerfile configurato.
3. Per ogni cartella presente dentro `gen` istanzia un container con
`wash-deploy-image` come immagine e monta la cartella all'interno
del container.
4. Attende la terminazione dei container.
#### Wash deploy image
Il Dockerfile utilizzato per buildare l'immagine `wash-deploy-image` è
più semplice, in quanto deve solamente installare la wasmCloud shell e
le sue dipendenze:
```dockerfile
FROM ubuntu:24.04 AS wash-deploy-image
# Install dependencies and tools
RUN apt-get update && apt-get install -y curl wget tar ...
# ----------------- Install WasmCloud -----------------
RUN curl -s "https://packagecloud.io/install/repositories/wasmcloud/core/script.deb.sh" | bash && \
apt-get install -y wash
# ----------------- Deploy the WasmCloud module -----------------
FROM wash-deploy-image
RUN mkdir /app
WORKDIR /app
# Deploy the WasmCloud module
CMD ["sh", "-c", "wash app deploy wadm.yaml"]
```
### Deployer
Il comando riportato nell'istruzione CMD in questo caso è
`wash app deploy wadm.yaml`, che utilizza il file wadm.yaml per creare
un'applicazione sul cluster wasmCloud specificato.\
L'approccio utilizzato per eseguire i processi di deployment è analogo a
quello della fase Build, la differenza sta nelle variabili d'ambiente
necessarie all'operazione: in questo caso sarà necessario fornire
hostname e porta di un server NATS collegato al cluster wasmCloud. Di
seguito viene riportata la porzione di codice che si occupa di eseguire
il container.\
```go
def __deploy_wadm(task_dir, client, nats_host, nats_port, detached, wait_list):
path = os.path.abspath(task_dir) + '/wadm.yaml'
name = wadm['spec']['components'][0]['name'] + '-deploy'
...
# Deploy wasmCloud app
print(f" - Deploying WASM module {name}")
container = client.containers.run(
"wash-deploy-image:latest",
environment=[f'WASMCLOUD_CTL_HOST={nats_host}',
f'WASMCLOUD_CTL_PORT={nats_port}'],
volumes={path: {'bind': '/app/wadm.yaml', 'mode': 'rw'}},
remove=True,
detach=True,
name=name
)
if detached == 'False':
container.wait()
else:
wait_list.append(name)
```
### Remover
Nel package `component_deploy` è presente anche la funzionalità
`remove`, con codice e comportamenti analoghi a quella di deploy.
L'unica differenza si presenta nel dockerfile, nel quale l'istruzione
specificata nel CMD è `wash app remove wadm.yaml` e permette di
rimuovere le applicazioni specificate sui file wadm dal cluster.\
## Processo completo
Anche in questo caso è possibile parallelizzare l'esecuzione dei
container, sia in fase di deployment che di rimozione delle
applicazioni. L'intero processo di deployment viene mostrato nella
seguente figura.
![Processo deployment applicazione su
wasmCloud](res/img/schemi-implementazione-deploy.drawio.svg)