Immagine di uno Starter Kit Kubernetes generata con Amazon Bedrock
Integrare un Large Language Model (LLM) in un'applicazione può renderla più potente e semplice da utilizzare, ma farlo in modo coerente con una codebase esistente e in maniera scalabile può presentare diverse sfide.
In questo articolo le proponiamo un progetto starter pensato proprio per partire in fretta.
Perché basare uno starter kit su Go?
La maggior parte degli esempi di Generative AI che si trovano in rete è scritta in Python, e ha senso: Python è la lingua franca del Machine Learning e della Data Science. Ma cosa fare se ha già un'applicazione che desidera semplicemente arricchire con modelli di Machine Learning tramite chiamate API standard, senza sviluppare modelli propri o affrontare attività dedicate al Machine Learning?
Go è un linguaggio ideale per effettuare chiamate verso Web Service, incluse API di Machine Learning come Amazon Bedrock o ChatGPT.
Lo Starter Kit descritto in questo articolo è incentrato su Amazon Bedrock ed è disponibile qui:
https://github.com/p-obrien/bedrock-microservice-starter
Prerequisito — Abilitare i modelli di Amazon Bedrock
Per utilizzare Amazon Bedrock occorre abilitare i Foundational Model di interesse; al momento (luglio 2024) questa operazione è purtroppo possibile solo dalla AWS Console.
Per abilitare il foundational model segua questi passaggi:
1. Acceda alla AWS Console e si rechi nella sezione Amazon Bedrock
2. Espanda il pannello di navigazione a sinistra e scorra fino a "Model access"
3. Nella finestra "Find model" cerchi il modello da abilitare; per il progetto starter useremo "Claude 3 Sonnet". Se non risulta abilitato per il suo progetto, dovrà richiedere l'accesso. Clicchi sul link "request model access", selezioni i modelli da abilitare e prosegua nelle schermate successive.
Una volta abilitato, dovrebbe presentarsi così:

Modelli base di AWS Bedrock
Nota: la disponibilità dei modelli varia in base alla region; potrebbe quindi rendersi necessario eseguire il modello in una region diversa da quella dei suoi workloads abituali.
**Panoramica dello Starter Kit**
Lo starter kit è pensato come punto di riferimento da riutilizzare nella sua soluzione e svolge le seguenti operazioni:
- Effettua il deploy di un cluster EKS basato su Graviton con un managed node group
- Effettua il deploy dell'AWS Load Balancer Controller
- Effettua il deploy di EKS Pod Identity e del relativo service account
Il codice Go esegue il deploy di un Microservice che:
- Richiede una API Key per autenticare i chiamanti
- Utilizza EKS Pod Identity per comunicare con Amazon Bedrock
- Restituisce una conversazione con Amazon Bedrock, in particolare con il foundational model Claude Sonnet, facilmente personalizzabile
Se dispone già di un cluster EKS può eseguire il codice direttamente al suo interno, ma dovrà assicurarsi di aver installato il Pod Identities Agent e il Service Account.
**Infrastruttura**
Lo starter kit utilizza Terraform o OpenTofu per il deploy di un nuovo cluster AWS EKS; il file README.md nella cartella Infrastructure illustra in dettaglio come usarlo.
Anziché ricorrere ad Access Key in un container, secret o altri metodi di autenticazione, sfrutteremo una funzionalità di AWS EKS chiamata Pod Identities per concedere l'accesso a Bedrock.
Fino a poco tempo fa per questo scopo si utilizzava EKS IRSA (IAM Roles for Service Accounts), ma la sua configurazione era piuttosto complessa e presentava limitazioni; EKS Pod Identities supera tali limiti con una soluzione semplice da implementare. Per chi volesse approfondire, i dettagli sono QUI.
EKS Pod Identity
Step 1 — Effettui il deploy dell'add-on EKS Pod Identity

Step 2 — Definisca un ruolo e consenta all'add-on Pod Identity di assumerlo

Step 3 — Associ un Kubernetes Service Account al ruolo

Step 4 — Verifichi che EKS abbia un service account corrispondente

Codice
La comunicazione con Amazon Bedrock dall'interno del Microservice avviene tramite l'Amazon Bedrock Go API.
Nota: sono possibili interazioni ben più complesse di quelle illustrate in questo progetto starter.
Autenticazione, in modo semplice
Il vantaggio di EKS Pod Identities è che risulta in larga misura trasparente per la sua applicazione. Il codice qui sotto, ad esempio, utilizza la Go Bedrock API per creare un client che sfrutta l'autenticazione tramite EKS Pod Identities:
func init() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
brc = bedrockruntime.NewFromConfig(cfg)
}
Anziché doversi preoccupare di caricare un secret, il pod ha già le variabili d'ambiente del profilo AWS impostate automaticamente, quindi le basta inizializzare il client Bedrock con i valori di default.
Server Rest
Per creare un endpoint Rest autenticato che riceve la richiesta dal chiamante utilizziamo l'Echo Framework; nel progetto starter ci affidiamo a una semplice API key:
// Sample API Key please don't use this in production and consider something more robust
var apiKey string = "test-api-key"
var brc *bedrockruntime.Client
const modelID = "anthropic.claude-3-sonnet-20240229-v1:0"
func main() {
cfg, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
log.Fatal(err)
}
brc = bedrockruntime.NewFromConfig(cfg)
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return key == apiKey, nil
}))
e.POST("/converse", converseHandler)
if err := e.Start(":8080"); err != http.ErrServerClosed {
log.Fatal(err)
}
}
A questo punto recuperiamo il prompt dall'input Multipart Form e lo inoltriamo a Bedrock, prima di restituire la risposta al client.
func converseHandler(c echo.Context) error {
// Initialize the Bedrock ConverseInput with the model ID
converseInput := &bedrockruntime.ConverseInput{
ModelId: aws.String(modelID),
}
// Get the user input from the form value
input := c.FormValue("message")
// Create the user's message
userMsg := types.Message{
Role: types.ConversationRoleUser,
Content: []types.ContentBlock{
&types.ContentBlockMemberText{
Value: input,
},
},
}
// Append the user's message to the conversation input
converseInput.Messages = append(converseInput.Messages, userMsg)
// Call the Bedrock Converse API
output, err := brc.Converse(context.Background(), converseInput)
if err != nil {
log.Fatal("Error calling Converse API:", err)
return c.String(http.StatusInternalServerError, "Internal Server Error")
}
// Extract the response from the assistant
response, ok := output.Output.(*types.ConverseOutputMemberMessage)
if !ok {
return c.String(http.StatusInternalServerError, "Failed to parse response")
}
responseContentBlock := response.Value.Content[0]
text, ok := responseContentBlock.(*types.ContentBlockMemberText)
if !ok {
return c.String(http.StatusInternalServerError, "Failed to parse response content")
}
// Create the assistant's message
assistantMsg := types.Message{
Role: types.ConversationRoleAssistant,
Content: response.Value.Content,
}
// Append the assistant's message to the conversation input
converseInput.Messages = append(converseInput.Messages, assistantMsg)
// Return the assistant's response to the client
return c.JSON(http.StatusOK, text.Value)
}
Possibili evoluzioni
Lasciamo al lettore, come esercizio, alcuni spunti di miglioramento:
- Risposte in streaming — l'Echo Framework supporta lo streaming e sarebbe interessante ottenere risposte in streaming anche da Bedrock
- System Prompt — aggiungere un system prompt per inizializzare Amazon Bedrock con istruzioni specifiche prima di gestire le interazioni con il client
Per concludere…
Grazie per la lettura. Per domande o feedback non esiti ad aprire un issue sul mio repository Github.
Se ancora non conosce DoiT International, la invitiamo a scoprirci. Il nostro team è pronto ad ascoltarla e a comprendere le sue esigenze di cloud engineering. Composto esclusivamente da senior Engineers, è specializzato in cloud consulting avanzato, progettazione architetturale e supporto al debugging. Ci contatti: ne parliamo insieme!