feat(azure): add infra + build/deploy scripts for Azure Container Apps

Split Bicep into infra.bicep (one-time: ACR + Log Analytics + Env) and
container-app.bicep (per-deploy: Container App with ACR auth).
Add 1-infra.sh and 2-build-deploy.sh shell scripts with config.sh.example
covering all .env variables. Gitignore azure/config.sh to prevent
secret leakage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-04-24 19:28:21 +00:00
parent b5c4d4a336
commit ffe6c537d0
6 changed files with 416 additions and 64 deletions

3
.gitignore vendored
View File

@ -60,3 +60,6 @@ backend/uploads/
data/
# Git worktrees
.worktrees/
# Configuració Azure amb secrets (no comitejar mai)
azure/config.sh

98
azure/1-infra.sh Executable file
View File

@ -0,0 +1,98 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# 1-infra.sh — Crea la infraestructura base de MiroFish a Azure
#
# Executa UNA SOLA VEGADA (o si vols recrear la infraestructura).
# Idempotent: pot executar-se múltiples vegades sense errors.
#
# Prerequisites:
# - az login executat
# - azure/config.sh existent (còpia de config.sh.example)
#
# Crea:
# - Resource Group: rg_mirofish
# - Azure Container Registry (ACR): ${PROJECT_NAME}acr
# - Log Analytics Workspace: ${PROJECT_NAME}-logs
# - Container Apps Environment: ${PROJECT_NAME}-env
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ── Carregar configuració ─────────────────────────────────────────────────────
CONFIG_FILE="${SCRIPT_DIR}/config.sh"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "ERROR: No s'ha trobat azure/config.sh"
echo " Còpia l'exemple: cp azure/config.sh.example azure/config.sh"
echo " Després omple els valors i torna a executar."
exit 1
fi
# shellcheck source=config.sh.example
source "$CONFIG_FILE"
# ── Validar variables obligatòries ───────────────────────────────────────────
REQUIRED_VARS=(
AZURE_SUBSCRIPTION_ID AZURE_LOCATION
RESOURCE_GROUP PROJECT_NAME
)
for var in "${REQUIRED_VARS[@]}"; do
if [[ -z "${!var:-}" ]]; then
echo "ERROR: La variable $var no està configurada a config.sh"
exit 1
fi
done
ACR_NAME="${PROJECT_NAME}acr"
echo "════════════════════════════════════════════════════════"
echo " MiroFish — Creació d'infraestructura Azure"
echo "════════════════════════════════════════════════════════"
echo " Subscripció : $AZURE_SUBSCRIPTION_ID"
echo " Localització: $AZURE_LOCATION"
echo " Grup recurs : $RESOURCE_GROUP"
echo " ACR : $ACR_NAME"
echo "════════════════════════════════════════════════════════"
echo ""
# ── Seleccionar subscripció ───────────────────────────────────────────────────
echo "→ Seleccionant subscripció..."
az account set --subscription "$AZURE_SUBSCRIPTION_ID"
# ── Registrar proveïdors necessaris ──────────────────────────────────────────
echo "→ Registrant proveïdors Azure (pot trigar uns minuts la primera vegada)..."
az provider register --namespace Microsoft.App --wait
az provider register --namespace Microsoft.OperationalInsights --wait
az provider register --namespace Microsoft.ContainerRegistry --wait
# ── Crear Resource Group ──────────────────────────────────────────────────────
echo "→ Creant Resource Group '$RESOURCE_GROUP'..."
az group create \
--name "$RESOURCE_GROUP" \
--location "$AZURE_LOCATION" \
--output none
echo " ✓ Resource Group llest"
# ── Desplegar infraestructura via Bicep ──────────────────────────────────────
echo "→ Desplegant infraestructura (ACR + Log Analytics + Container Apps Env)..."
INFRA_OUTPUT=$(az deployment group create \
--resource-group "$RESOURCE_GROUP" \
--template-file "${SCRIPT_DIR}/infra.bicep" \
--parameters \
projectName="$PROJECT_NAME" \
location="$AZURE_LOCATION" \
--output json)
# Extreure outputs del desplegament
ACR_LOGIN_SERVER=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['acrLoginServer']['value'])")
ACR_NAME_OUT=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['acrName']['value'])")
ENV_ID=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['containerAppsEnvId']['value'])")
echo ""
echo "════════════════════════════════════════════════════════"
echo " Infraestructura creada correctament!"
echo "════════════════════════════════════════════════════════"
echo " ACR Login Server : $ACR_LOGIN_SERVER"
echo " Container Apps Env ID: $ENV_ID"
echo ""
echo " Proper pas: bash azure/2-build-deploy.sh"
echo "════════════════════════════════════════════════════════"

147
azure/2-build-deploy.sh Executable file
View File

@ -0,0 +1,147 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# 2-build-deploy.sh — Build Docker + push a ACR + deploy Container App
#
# Executar a cada nova versió de l'aplicació.
# Requereix que 1-infra.sh hagi estat executat prèviament.
#
# Prerequisites:
# - az login executat
# - azure/config.sh existent i configurat
# - Docker instal·lat i en execució
# - Infraestructura creada (azure/1-infra.sh)
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# ── Carregar configuració ─────────────────────────────────────────────────────
CONFIG_FILE="${SCRIPT_DIR}/config.sh"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "ERROR: No s'ha trobat azure/config.sh"
echo " Còpia l'exemple: cp azure/config.sh.example azure/config.sh"
exit 1
fi
# shellcheck source=config.sh.example
source "$CONFIG_FILE"
# ── Validar variables obligatòries ───────────────────────────────────────────
REQUIRED_VARS=(
AZURE_SUBSCRIPTION_ID RESOURCE_GROUP PROJECT_NAME
DEMO_PASSWORD SECRET_KEY LLM_API_KEY LLM_BASE_URL LLM_MODEL_NAME ZEP_API_KEY
)
for var in "${REQUIRED_VARS[@]}"; do
if [[ -z "${!var:-}" ]]; then
echo "ERROR: La variable $var no està configurada a config.sh"
exit 1
fi
done
ACR_NAME="${PROJECT_NAME}acr"
# ── Seleccionar subscripció ───────────────────────────────────────────────────
echo "→ Seleccionant subscripció..."
az account set --subscription "$AZURE_SUBSCRIPTION_ID"
# ── Obtenir dades de la infraestructura existent ──────────────────────────────
echo "→ Obtenint dades de la infraestructura..."
ACR_LOGIN_SERVER=$(az acr show \
--name "$ACR_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query loginServer --output tsv)
ENV_ID=$(az containerapp env show \
--name "${PROJECT_NAME}-env" \
--resource-group "$RESOURCE_GROUP" \
--query id --output tsv)
if [[ -z "$ACR_LOGIN_SERVER" || -z "$ENV_ID" ]]; then
echo "ERROR: No s'ha trobat la infraestructura. Executa primer: bash azure/1-infra.sh"
exit 1
fi
# ── Generar tag de versió ─────────────────────────────────────────────────────
# Format: <git-sha-curt>-<timestamp> per a traçabilitat
GIT_SHA=$(git -C "$REPO_ROOT" rev-parse --short HEAD 2>/dev/null || echo "nogit")
TIMESTAMP=$(date +%Y%m%d%H%M)
IMAGE_TAG="${GIT_SHA}-${TIMESTAMP}"
FULL_IMAGE="${ACR_LOGIN_SERVER}/${PROJECT_NAME}:${IMAGE_TAG}"
LATEST_IMAGE="${ACR_LOGIN_SERVER}/${PROJECT_NAME}:latest"
echo ""
echo "════════════════════════════════════════════════════════"
echo " MiroFish — Build & Deploy"
echo "════════════════════════════════════════════════════════"
echo " ACR : $ACR_LOGIN_SERVER"
echo " Imatge : ${PROJECT_NAME}:${IMAGE_TAG}"
echo " Container Env : ${PROJECT_NAME}-env"
echo "════════════════════════════════════════════════════════"
echo ""
# ── Login a l'ACR ─────────────────────────────────────────────────────────────
echo "→ Login a l'ACR..."
az acr login --name "$ACR_NAME"
# ── Build de la imatge Docker ─────────────────────────────────────────────────
echo "→ Build de la imatge Docker..."
docker build \
--tag "$FULL_IMAGE" \
--tag "$LATEST_IMAGE" \
"$REPO_ROOT"
echo " ✓ Build completat"
# ── Push de la imatge a l'ACR ─────────────────────────────────────────────────
echo "→ Push a l'ACR ($FULL_IMAGE)..."
docker push "$FULL_IMAGE"
docker push "$LATEST_IMAGE"
echo " ✓ Push completat"
# ── Obtenir credencials ACR per al Bicep ─────────────────────────────────────
echo "→ Obtenint credencials ACR..."
ACR_USERNAME=$(az acr credential show \
--name "$ACR_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query username --output tsv)
ACR_PASSWORD=$(az acr credential show \
--name "$ACR_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "passwords[0].value" --output tsv)
# ── Desplegar Container App via Bicep ─────────────────────────────────────────
echo "→ Desplegant Container App..."
DEPLOY_OUTPUT=$(az deployment group create \
--resource-group "$RESOURCE_GROUP" \
--template-file "${SCRIPT_DIR}/container-app.bicep" \
--parameters \
projectName="$PROJECT_NAME" \
containerAppsEnvId="$ENV_ID" \
containerImage="$FULL_IMAGE" \
acrLoginServer="$ACR_LOGIN_SERVER" \
acrUsername="$ACR_USERNAME" \
acrPassword="$ACR_PASSWORD" \
demoPassword="$DEMO_PASSWORD" \
llmApiKey="$LLM_API_KEY" \
llmBoostApiKey="${LLM_BOOST_API_KEY:-}" \
zepApiKey="$ZEP_API_KEY" \
secretKey="$SECRET_KEY" \
llmBaseUrl="$LLM_BASE_URL" \
llmModelName="$LLM_MODEL_NAME" \
llmBoostBaseUrl="${LLM_BOOST_BASE_URL:-}" \
llmBoostModelName="${LLM_BOOST_MODEL_NAME:-}" \
oasisDefaultMaxRounds="${OASIS_DEFAULT_MAX_ROUNDS:-10}" \
reportAgentMaxToolCalls="${REPORT_AGENT_MAX_TOOL_CALLS:-5}" \
reportAgentMaxReflectionRounds="${REPORT_AGENT_MAX_REFLECTION_ROUNDS:-2}" \
reportAgentTemperature="${REPORT_AGENT_TEMPERATURE:-0.5}" \
--output json)
FQDN=$(echo "$DEPLOY_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['containerAppFqdn']['value'])")
echo ""
echo "════════════════════════════════════════════════════════"
echo " Deploy completat!"
echo "════════════════════════════════════════════════════════"
echo " URL de l'aplicació: https://$FQDN"
echo " Imatge desplegada : $FULL_IMAGE"
echo "════════════════════════════════════════════════════════"

51
azure/config.sh.example Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Configuració de desplegament MiroFish a Azure
#
# INSTRUCCIONS:
# 1. Còpia aquest fitxer a azure/config.sh (NO comitegis config.sh — té secrets)
# cp azure/config.sh.example azure/config.sh
# 2. Omple tots els valors marcats amb <...>
# 3. Executa: az login
# 4. Executa: bash azure/1-infra.sh (una sola vegada)
# 5. Executa: bash azure/2-build-deploy.sh (a cada nova versió)
# ─────────────────────────────────────────────────────────────────────────────
# ── Subscripció i localització Azure ─────────────────────────────────────────
AZURE_SUBSCRIPTION_ID="<la-teva-subscription-id>"
AZURE_LOCATION="westeurope" # canvia si prefereixes altra regió
# ── Noms de recursos (pots deixar els valors per defecte) ─────────────────────
RESOURCE_GROUP="rg_mirofish"
PROJECT_NAME="mirofish" # prefix per a tots els recursos Azure
# Nota: el nom de l'ACR serà "${PROJECT_NAME}acr" (sense guions, tot minúscula)
# ── Secrets de l'aplicació ────────────────────────────────────────────────────
# Contrasenya de l'usuari "demo" per fer login a l'app
DEMO_PASSWORD="<contrasenya-segura>"
# Flask SECRET_KEY per signar tokens JWT
# Genera-la amb: python -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY="<flask-secret-key>"
# ── LLM principal (OpenAI-compatible) ─────────────────────────────────────────
LLM_API_KEY="<la-teva-llm-api-key>"
LLM_BASE_URL="https://dashscope.aliyuncs.com/compatible-mode/v1"
LLM_MODEL_NAME="qwen-plus"
# ── LLM accelerador (opcional — deixar buit per desactivar) ───────────────────
LLM_BOOST_API_KEY=""
LLM_BOOST_BASE_URL=""
LLM_BOOST_MODEL_NAME=""
# ── Zep Cloud (graf de memòria) ───────────────────────────────────────────────
ZEP_API_KEY="<la-teva-zep-api-key>"
# ── Simulació OASIS (valors per defecte recomanats) ───────────────────────────
OASIS_DEFAULT_MAX_ROUNDS="10"
# ── Report Agent (valors per defecte recomanats) ──────────────────────────────
REPORT_AGENT_MAX_TOOL_CALLS="5"
REPORT_AGENT_MAX_REFLECTION_ROUNDS="2"
REPORT_AGENT_TEMPERATURE="0.5"

View File

@ -1,34 +1,42 @@
// ─────────────────────────────────────────────────────────────────────────────
// MiroFish — Azure Container App
// Segueix les directrius CTTI per a desplegament d'aplicacions a Azure
// (DA/TM: Azure Container Apps com a plataforma de desplegament de contenidors)
// MiroFish — Container App (executar a cada deploy)
//
// Paràmetres que l'equip d'ops ha de proporcionar en desplegar:
// Secrets (@secure): demoPassword, llmApiKey, llmBoostApiKey, zepApiKey, secretKey
// Valors: containerImage, llmBaseUrl, llmModelName, llmBoostBaseUrl,
// llmBoostModelName, oasisDefaultMaxRounds,
// reportAgentMaxToolCalls, reportAgentMaxReflectionRounds,
// reportAgentTemperature
// Rep com a paràmetres els outputs d'infra.bicep (containerAppsEnvId,
// acrLoginServer) i desplega/actualitza la Container App amb la nova imatge.
//
// Executar amb: azure/2-build-deploy.sh
//
// Extensions pendents per a l'equip d'operacions:
// - DNS: afegir CNAME a *.intranet.gencat.cat (PRE) / *.gencat.cat (PRO)
// - Xarxa: integrar en VNet Hub-Spoke + Private Link per a serveis interns
// (descomentar el bloc vnetConfiguration a l'entorn)
// - TLS: certificat gestionat via Container Apps o Azure Front Door
// - ingress.external: canviar a false per a accés exclusiu per intranet
// - TLS: certificat gestionat via Container Apps o Azure Front Door
// ─────────────────────────────────────────────────────────────────────────────
@description('Nom base del projecte (es fa servir per als noms dels recursos)')
@description('Nom base del projecte')
param projectName string = 'mirofish'
@description('Localització Azure dels recursos')
param location string = resourceGroup().location
@description('Imatge Docker completa (registry/imatge:tag)')
@description('ID del Container Apps Environment (output d\'infra.bicep)')
param containerAppsEnvId string
@description('Imatge Docker completa (acrLoginServer/nom:tag)')
param containerImage string
@description('Login server de l\'ACR (ex: mirofsihacr.azurecr.io)')
param acrLoginServer string
// ─── Paràmetres secrets (@secure — mai visibles als logs de desplegament) ────
@description('Nom d\'usuari de l\'ACR (az acr credential show --name <acr> --query username)')
@secure()
param acrUsername string
@description('Contrasenya de l\'ACR (az acr credential show --name <acr> --query passwords[0].value)')
@secure()
param acrPassword string
@description('Contrasenya de l\'usuari demo')
@secure()
param demoPassword string
@ -45,7 +53,7 @@ param llmBoostApiKey string = ''
@secure()
param zepApiKey string
@description('SECRET_KEY de Flask per a JWT (generar amb: python -c "import secrets; print(secrets.token_hex(32))")')
@description('SECRET_KEY de Flask per a JWT (python -c "import secrets; print(secrets.token_hex(32))")')
@secure()
param secretKey string
@ -81,54 +89,31 @@ param reportAgentMaxReflectionRounds string = '2'
@description('Temperatura del model LLM per al Report Agent')
param reportAgentTemperature string = '0.5'
// ─── Log Analytics Workspace ──────────────────────────────────────────────────
// NOR0016-C: retenció mínima de logs de seguretat = 90 dies
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: '${projectName}-logs'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 90
}
}
// ─── Container Apps Environment ───────────────────────────────────────────────
resource containerAppsEnv 'Microsoft.App/managedEnvironments@2023-05-01' = {
name: '${projectName}-env'
location: location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
// TODO (ops): descomentar per integrar en VNet Hub-Spoke CTTI
// vnetConfiguration: {
// infrastructureSubnetId: '/subscriptions/.../subnets/container-apps-subnet'
// internal: true // true = accés únicament per intranet
// }
}
}
// ─── Container App ─────────────────────────────────────────────────────────────
resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
name: projectName
location: location
properties: {
managedEnvironmentId: containerAppsEnv.id
managedEnvironmentId: containerAppsEnvId
configuration: {
// Secrets de Container Apps — mai en text pla a les variables d'entorn
// Secrets: credencials ACR + variables sensibles de l'aplicació
secrets: [
{ name: 'demo-password', value: demoPassword }
{ name: 'llm-api-key', value: llmApiKey }
{ name: 'llm-boost-api-key', value: llmBoostApiKey }
{ name: 'zep-api-key', value: zepApiKey }
{ name: 'secret-key', value: secretKey }
{ name: 'acr-password', value: acrPassword }
{ name: 'demo-password', value: demoPassword }
{ name: 'llm-api-key', value: llmApiKey }
{ name: 'llm-boost-api-key', value: llmBoostApiKey }
{ name: 'zep-api-key', value: zepApiKey }
{ name: 'secret-key', value: secretKey }
]
// Credencials del registre privat (ACR)
registries: [
{
server: acrLoginServer
username: acrUsername
passwordSecretRef: 'acr-password'
}
]
// Ingrés: port únic 5001 (Flask serveix frontend + API)
@ -138,11 +123,7 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
transport: 'http'
// TODO (ops): afegir domini corporatiu quan estigui assignat
// customDomains: [
// {
// name: 'mirofish.intranet.gencat.cat' // PRE
// certificateId: '<id-certificat-aca>'
// bindingType: 'SniEnabled'
// }
// { name: 'mirofish.intranet.gencat.cat', certificateId: '...', bindingType: 'SniEnabled' }
// ]
}
@ -210,11 +191,8 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
}
// ─── Outputs ──────────────────────────────────────────────────────────────────
@description('FQDN de l\'aplicació desplegada')
@description('FQDN públic de l\'aplicació')
output containerAppFqdn string = containerApp.properties.configuration.ingress.fqdn
@description('Nom del recurs Container App')
output containerAppName string = containerApp.name
@description('ID del workspace de Log Analytics')
output logAnalyticsWorkspaceId string = logAnalytics.id

75
azure/infra.bicep Normal file
View File

@ -0,0 +1,75 @@
// ─────────────────────────────────────────────────────────────────────────────
// MiroFish — Infraestructura base (executar una sola vegada)
//
// Crea:
// - Azure Container Registry (ACR) per emmagatzemar la imatge Docker
// - Log Analytics Workspace (NOR0016-C: 90 dies retenció)
// - Container Apps Environment (plataforma d'execució CTTI)
//
// Executar amb: azure/1-infra.sh
// ─────────────────────────────────────────────────────────────────────────────
@description('Nom base del projecte')
param projectName string = 'mirofish'
@description('Localització Azure dels recursos')
param location string = resourceGroup().location
// ─── Azure Container Registry ─────────────────────────────────────────────────
// SKU Basic: suficient per a imatges privades sense geo-replicació
resource acr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = {
name: '${projectName}acr' // ACR no admet guions, tot minúscula
location: location
sku: {
name: 'Basic'
}
properties: {
adminUserEnabled: true // necessari per a la autenticació des dels scripts
}
}
// ─── Log Analytics Workspace ──────────────────────────────────────────────────
// NOR0016-C: retenció mínima de logs de seguretat = 90 dies
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2022-10-01' = {
name: '${projectName}-logs'
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 90
}
}
// ─── Container Apps Environment ───────────────────────────────────────────────
resource containerAppsEnv 'Microsoft.App/managedEnvironments@2023-05-01' = {
name: '${projectName}-env'
location: location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
// TODO (ops): descomentar per integrar en VNet Hub-Spoke CTTI
// vnetConfiguration: {
// infrastructureSubnetId: '/subscriptions/.../subnets/container-apps-subnet'
// internal: true
// }
}
}
// ─── Outputs (usats pels scripts de deploy) ───────────────────────────────────
@description('URL de login de l\'ACR (ex: mirofsihacr.azurecr.io)')
output acrLoginServer string = acr.properties.loginServer
@description('Nom del recurs ACR (per a az acr build)')
output acrName string = acr.name
@description('ID del Container Apps Environment')
output containerAppsEnvId string = containerAppsEnv.id
@description('ID del Log Analytics Workspace')
output logAnalyticsId string = logAnalytics.id