Questo articolo descrive un assistente SRE AI costruito con l’SDK Strands Agents, Django, React e AWS. L’agente indaga autonomamente i problemi infrastrutturali combinando ragionamento LLM con integrazioni profonde in GitLab e AWS.
Il Problema#
I team di piattaforma su larga scala affrontano una sfida ricorrente: investigare i problemi di produzione richiede correlare informazioni su più sistemi. Una tipica investigazione potrebbe essere così:
- Qualcuno segnala un errore 503 su un servizio
- Controlli lo stato del target group ALB
- I target sono non-sani, quindi controlli il servizio ECS
- ECS mostra un deployment in corso, quindi controlli la task definition
- Il tag dell’immagine punta a un commit, quindi controlli GitLab
- L’ultima MR ha cambiato un endpoint di health check, quindi leggi il codice
- Il codice ha un bug nel nuovo health check, quindi controlli i log di CloudWatch per confermare
- I log confermano l’errore, scrivi il report
Ogni passo richiede di cambiare contesto, autenticarsi su un sistema diverso, navigare un’interfaccia diversa e correlare mentalmente i risultati. Per un ingegnere esperto, questo richiede 15-30 minuti. Per uno junior, possono volerci ore.
L’assistente SRE automatizza questo intero flusso. Digiti “investiga gli errori 503 sul servizio X” e l’agente autonomamente trova il repo in GitLab, legge la struttura e la configurazione del repository, identifica l’account e la regione AWS, controlla la salute ALB, lo stato ECS e i deployment recenti, legge i log CloudWatch per pattern di errore e correla tutti i risultati in un report strutturato.
L’intuizione chiave è che l’investigazione SRE è in gran parte procedurale. I passi seguono pattern. Quello che rende bravo un buon SRE non è memorizzare quei pattern, ma sapere quali applicare e come interpretare i risultati. È esattamente ciò in cui eccellono gli LLM.
Panoramica dell’Architettura#
L’assistente è un monolite distribuito come singolo container:
flowchart TD
VPN([VPN]) -->|HTTPS 443| ALB[Internal ALB\nTLS 1.3]
ALB --> Fargate[ECS Fargate\nport 8080]
Fargate --> Nginx[Nginx]
Nginx -->|/assets/*| SPA[React SPA\nfile statici]
Nginx -->|/api/*| Uvicorn[Uvicorn\nDjango ASGI\nport 8000]
Uvicorn --> PG[(Aurora PostgreSQL\nServerless v2)]
Uvicorn --> Redis[(ElastiCache\nRedis)]
Uvicorn --> Bedrock[AWS Bedrock\nAgentCore Memory]
Uvicorn --> LLM[LLM Gateway\nOAuth2]
Il frontend è una SPA React servita da Nginx. Le richieste API sono proxate al backend Django in esecuzione su Uvicorn con supporto ASGI. PostgreSQL archivia utenti, sessioni, cronologia chat e snapshot KPI. Redis fa cache delle credenziali AWS e delle risposte GitLab.
Perché un monolite? Perché la complessità operativa di un’architettura a microservizi non è giustificata quando hai un singolo team, un singolo target di deployment e una singola applicazione. Il monolite si distribuisce in secondi, ha un unico set di log da controllare e un unico servizio da monitorare.
Stack Tecnologico#
- Python 3.12+ con Django 6.0
- Supporto async via
adrf - Uvicorn come server ASGI
strands-agentsper il framework agente AIhttpxper client HTTP asincronimsalper Microsoft Entra ID OAuth2
- React 19 con TypeScript 5
- Vite 7 come build tool
- Tailwind CSS 3
- Radix UI per primitive accessibili
- Framer Motion per le animazioni
- Server-Sent Events per lo streaming real-time
- AWS ECS Fargate (Spot + On-Demand)
- Aurora PostgreSQL Serverless v2
- ElastiCache Redis
- Internal ALB con WAF
- Terraform per IaC
- Docker multi-stage build
Autenticazione: Quattro Layer in Profondità#
L’autenticazione non riguarda solo l’identificazione dell’utente. Riguarda la creazione di catene di fiducia su quattro sistemi distinti.
Layer 1: Microsoft Entra ID (Identità Utente)#
L’autenticazione primaria usa il flusso OAuth2 Authorization Code con PKCE:
class AuthCallbackView(APIView):
async def post(self, request):
code = request.data["code"]
code_verifier = request.data["code_verifier"]
msal_app = ConfidentialClientApplication(
client_id=settings.ENTRA_CLIENT_ID,
authority=f"https://login.microsoftonline.com/{settings.ENTRA_TENANT_ID}",
client_credential=settings.ENTRA_CLIENT_SECRET,
)
result = msal_app.acquire_token_by_authorization_code(
code=code,
scopes=["User.Read"],
redirect_uri=settings.ENTRA_REDIRECT_URI,
code_verifier=code_verifier,
)
id_token = validate_and_decode_token(result["id_token"])
user, _ = await User.objects.aupdate_or_create(
entra_id=id_token["oid"],
defaults={"display_name": id_token["name"]},
)
session = await UserSession.objects.acreate(
user=user,
expires_at=now() + timedelta(hours=24),
)
response = Response({"user": UserSerializer(user).data})
response.set_cookie(
"session_id", session.token,
httponly=True, secure=True, samesite="Strict",
)
return responseLayer 2: Federazione SAML AWS#
Questo è il layer di autenticazione più complesso. Gli ingegneri hanno bisogno di accesso a 100+ account AWS, ognuno con più ruoli:
sequenceDiagram
participant User
participant ADFS as Corporate ADFS
participant KC as Keycloak
participant STS as AWS STS
User->>ADFS: credenziali (NTLM)
ADFS-->>KC: asserzione SAML
KC->>User: sfida OTP
User->>KC: codice OTP
KC-->>STS: risposta SAML AWS
STS-->>User: Credenziali temporanee (TTL 1h)
Note over User,STS: Cache in Redis, auto-refresh
Quando il segreto OTP è configurato, l’assistente può ri-autenticarsi automaticamente quando le credenziali scadono. Le credenziali temporanee sono in cache in Redis con un TTL che corrisponde alla loro scadenza STS.
Il Sistema Agente#
Il cuore dell’assistente è l’agente, costruito sul framework strands-agents:
def build_agent(user, user_settings, session_id):
model = OpenAIModel(
model_id="gpt-5.1",
client_args={
"base_url": LLM_GATEWAY_URL,
"api_key": get_gateway_token(user_settings),
},
params={"temperature": 0.0},
)
tools = []
if user_settings.gitlab_token:
tools.extend(GITLAB_TOOLS)
if has_aws_credentials(user):
tools.extend(AWS_TOOLS)
tools.extend(CORE_TOOLS)
return Agent(
model=model,
tools=tools,
tool_executor=ConcurrentToolExecutor(),
conversation_manager=SlidingWindowConversationManager(window_size=40),
system_prompt=SYSTEM_PROMPT,
)La temperatura è impostata a 0.0 perché l’investigazione SRE richiede determinismo. Vuoi che l’agente segua le stesse procedure in modo costante.
Il System Prompt#
Il system prompt supera le 8.000 parole. Alcune direttive chiave:
Investigazione autonoma, non consigli:
“Quando viene chiesto di investigare, tu investighi. Non suggerire cosa dovrebbe controllare l’utente. Lo controlli tu stesso.”
Mandato context-first:
“Quando viene menzionato un nome di servizio, chiama SEMPRE gather_context() prima di qualsiasi analisi.”
Zero-hallucination:
“Non fare mai riferimento a risorse AWS o progetti GitLab che non hai scoperto tramite tool call in questa conversazione.”
Registry dei Tool: 30+ Tool Specializzati#
L’assistente ha accesso a oltre 30 tool, ognuno progettato per un compito specifico di investigazione.
@tool
def aws_ecs(
action: str,
parameters: dict,
account_id: str | None = None,
region: str = "eu-central-1",
) -> dict:
"""Interroga le risorse AWS ECS (sola lettura).
Azioni comuni: describe_services, describe_tasks, list_services,
describe_task_definition, list_task_definitions
"""
if not action.startswith(("describe_", "list_", "get_")):
return {"error": f"Azione '{action}' non consentita (sola lettura)"}
credentials = get_cached_aws_credentials(account_id)
client = boto3.client(
"ecs",
region_name=region,
aws_access_key_id=credentials["AccessKeyId"],
aws_secret_access_key=credentials["SecretAccessKey"],
aws_session_token=credentials["SessionToken"],
)
method = getattr(client, action)
return truncate_response(method(**parameters), max_chars=12000)Architettura Sub-Agente#
Le investigazioni complesse spesso richiedono flussi di lavoro paralleli. L’assistente usa un pattern sub-agente:
@tool
def gather_context(service_name: str) -> dict:
"""Scopre autonomamente tutto su un servizio."""
sub_agent = Agent(
model=model,
tools=[gitlab, gitlab_get_file, gitlab_list_tree, gitlab_search_group],
system_prompt=GATHER_CONTEXT_PROMPT,
)
result = sub_agent(
f"Trova e analizza il servizio '{service_name}'. "
f"Restituisci il project ID GitLab, account AWS, regione, "
f"nome del cluster e dettagli di configurazione chiave."
)
return parse_context(result)Infrastruttura di Streaming#
Lo streaming real-time è essenziale. Le investigazioni possono richiedere 30-60 secondi. Senza streaming, l’utente fissa uno spinner senza visibilità su cosa sta accadendo.
class ChatStreamView(APIView):
async def post(self, request):
agent = build_agent(request.user, request.user_settings, session_id)
async def event_stream():
queue = asyncio.Queue()
task = asyncio.create_task(run_agent(agent, message, queue))
while True:
event = await queue.get()
if event["type"] == "done":
yield format_sse(event)
break
if event["type"] in ("text_delta", "tool_start", "tool_end"):
yield format_sse(event)
await task
response = StreamingHttpResponse(
event_stream(),
content_type="text/event-stream",
)
response["X-Accel-Buffering"] = "no" # Disabilita il buffering Nginx
return responseL’header X-Accel-Buffering: no è critico. Senza di esso, Nginx bufferizza lo stream SSE e lo consegna a blocchi, distruggendo l’esperienza real-time.
Schema del Database#
erDiagram
User ||--|| UserSettings : has
User ||--o{ UserSession : has
User ||--o{ ChatSession : owns
ChatSession ||--o{ Message : contains
Message ||--o{ ToolInvocation : triggers
User {
uuid id PK
string entra_id
string display_name
string role
}
ChatSession {
uuid id PK
string title
string output_mode
}
Message {
uuid id PK
string role
text content
json metrics
}
ToolInvocation {
uuid id PK
string tool_name
json input_params
string status
}
Deployment#
resource "aws_ecs_service" "tars" {
name = "tars"
desired_count = 2
capacity_provider_strategy {
capacity_provider = "FARGATE_SPOT"
weight = 100
base = 1
}
deployment_circuit_breaker {
enable = true
rollback = true
}
}
resource "aws_ecs_task_definition" "tars" {
cpu = 1024
memory = 2048
runtime_platform {
cpu_architecture = "ARM64"
}
}Lezioni Imparate#
Temperatura 0.0 per gli agenti di investigazione
I sub-agenti sono essenziali per investigazioni complesse
Lo streaming non è opzionale
Il troncamento dell'output previene il context collapse
get_log_events di CloudWatch può restituire megabyte di dati. Senza troncamento aggressivo (12.000 caratteri), la context window si riempie di rumore e l’agente perde il filo dell’investigazione.Sola lettura per default, sempre
Credenziali per utente, mai condivise
Il system prompt è il prodotto
Cache alla giusta granularità
Cosa Viene Dopo#
L’evoluzione naturale è espandere dall’investigazione alla remediation. Oggi l’assistente ti dice cosa c’è di sbagliato. Domani potrebbe:
- Creare un branch hotfix con la modifica di codice corretta
- Aprire una MR con la fix, contrassegnata per revisione urgente
- Redigere il report dell’incidente basandosi sui risultati dell’investigazione
- Suggerire aggiornamenti al runbook in base ai nuovi modi di fallimento che scopre
Questo è l’endgame: un SRE AI che indaga come il tuo miglior ingegnere, comunica come il tuo miglior technical writer e non ha mai bisogno di dormire.