// ───────────────────────────────────────────────────────────────────────────── // MiroFish — Container App (executar a cada deploy) // // 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) // - 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') param projectName string = 'mirofish' @description('Localització Azure dels recursos') param location string = resourceGroup().location @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 --query username)') @secure() param acrUsername string @description('Contrasenya de l\'ACR (az acr credential show --name --query passwords[0].value)') @secure() param acrPassword string @description('Contrasenya de l\'usuari demo') @secure() param demoPassword string @description('Clau de l\'API LLM principal (OpenAI-compatible)') @secure() param llmApiKey string @description('Clau de l\'API LLM acceleradora (opcional, deixar buit si no s\'usa)') @secure() param llmBoostApiKey string = '' @description('Clau de l\'API Zep Cloud (obligatori si GRAPH_BACKEND=zep)') @secure() param zepApiKey string = '' @description('Contrasenya de Neo4j (obligatori si GRAPH_BACKEND=graphiti)') @secure() param neo4jPassword string = '' @description('SECRET_KEY de Flask per a JWT (python -c "import secrets; print(secrets.token_hex(32))")') @secure() param secretKey string @description('Connection string del Storage Account per a Azure Files (output d\'infra.bicep)') @secure() param storageConnectionString string = '' @description('DATABASE_URL PostgreSQL (output d\'infra.bicep: postgresql+psycopg2://...)') @secure() param databaseUrl string = '' @description('Nom del Storage Account (output d\'infra.bicep)') param storageAccountName string = '' // ─── Paràmetres LLM principal ───────────────────────────────────────────────── @description('URL base de l\'API LLM principal') param llmBaseUrl string = 'https://dashscope.aliyuncs.com/compatible-mode/v1' @description('Nom del model LLM principal') param llmModelName string = 'qwen-plus' @description('Proveïdor LLM (gemini per a Google AI Studio; buit per a qualsevol compatible OpenAI)') param llmProvider string = '' // ─── Paràmetres del backend de graf ────────────────────────────────────────── @description('Backend de graf: zep (Zep Cloud) o graphiti (Neo4j local/Azure)') param graphBackend string = 'zep' @description('URI de connexió bolt de Neo4j (necessari si GRAPH_BACKEND=graphiti)') param neo4jUri string = 'bolt://localhost:7687' @description('Usuari de Neo4j') param neo4jUser string = 'neo4j' // ─── Paràmetres LLM accelerador (opcionals) ────────────────────────────────── @description('URL base de l\'API LLM acceleradora (opcional)') param llmBoostBaseUrl string = '' @description('Nom del model LLM accelerador (opcional)') param llmBoostModelName string = '' // ─── Paràmetres LLM embeddings (opcionals — fallback a LLM principal) ───────── @description('Clau API LLM embeddings (opcional)') @secure() param llmEmbedApiKey string = '' @description('URL base LLM embeddings (opcional)') param llmEmbedBaseUrl string = '' @description('Nom del model d\'embeddings') param llmEmbedModelName string = '' // ─── Paràmetres LLM petit/ràpid (opcionals — fallback a LLM principal) ──────── @description('Clau API LLM petit (opcional)') @secure() param llmSmallApiKey string = '' @description('URL base LLM petit (opcional)') param llmSmallBaseUrl string = '' @description('Nom del model LLM petit (opcional)') param llmSmallModelName string = '' // ─── Paràmetres Neo4j addicionals ───────────────────────────────────────────── @description('Nom de la base de dades Neo4j') param neo4jDatabase string = 'neo4j' // ─── Paràmetres de simulació OASIS ─────────────────────────────────────────── @description('Nombre màxim de rondes per a la simulació OASIS') param oasisDefaultMaxRounds string = '10' // ─── Paràmetres del Report Agent ───────────────────────────────────────────── @description('Nombre màxim de crides a eines del Report Agent') param reportAgentMaxToolCalls string = '5' @description('Nombre màxim de rondes de reflexió del Report Agent') param reportAgentMaxReflectionRounds string = '2' @description('Temperatura del model LLM per al Report Agent') param reportAgentTemperature string = '0.5' // ─── Secrets i env vars condicionals (Azure rebutja secrets amb valor buit) ─── var mandatorySecrets = [ { name: 'acr-password', value: acrPassword } { name: 'demo-password', value: demoPassword } { name: 'llm-api-key', value: llmApiKey } { name: 'secret-key', value: secretKey } ] var optionalSecrets = concat( empty(llmBoostApiKey) ? [] : [{ name: 'llm-boost-api-key', value: llmBoostApiKey }], empty(llmEmbedApiKey) ? [] : [{ name: 'llm-embed-api-key', value: llmEmbedApiKey }], empty(llmSmallApiKey) ? [] : [{ name: 'llm-small-api-key', value: llmSmallApiKey }], empty(zepApiKey) ? [] : [{ name: 'zep-api-key', value: zepApiKey }], empty(neo4jPassword) ? [] : [{ name: 'neo4j-password', value: neo4jPassword }], empty(storageConnectionString) ? [] : [{ name: 'storage-connection-string', value: storageConnectionString }], empty(databaseUrl) ? [] : [{ name: 'database-url', value: databaseUrl }] ) var allSecrets = concat(mandatorySecrets, optionalSecrets) var mandatoryEnv = [ { name: 'DEMO_PASSWORD', secretRef: 'demo-password' } { name: 'LLM_API_KEY', secretRef: 'llm-api-key' } { name: 'SECRET_KEY', secretRef: 'secret-key' } { name: 'LLM_BASE_URL', value: llmBaseUrl } { name: 'LLM_MODEL_NAME', value: llmModelName } { name: 'LLM_PROVIDER', value: llmProvider } { name: 'LLM_BOOST_BASE_URL', value: llmBoostBaseUrl } { name: 'LLM_BOOST_MODEL_NAME', value: llmBoostModelName } { name: 'GRAPH_BACKEND', value: graphBackend } { name: 'NEO4J_URI', value: neo4jUri } { name: 'NEO4J_USER', value: neo4jUser } { name: 'NEO4J_DATABASE', value: neo4jDatabase } { name: 'LLM_EMBED_BASE_URL', value: llmEmbedBaseUrl } { name: 'LLM_EMBED_MODEL_NAME', value: llmEmbedModelName } { name: 'LLM_SMALL_BASE_URL', value: llmSmallBaseUrl } { name: 'LLM_SMALL_MODEL_NAME', value: llmSmallModelName } { name: 'OASIS_DEFAULT_MAX_ROUNDS', value: oasisDefaultMaxRounds } { name: 'REPORT_AGENT_MAX_TOOL_CALLS', value: reportAgentMaxToolCalls } { name: 'REPORT_AGENT_MAX_REFLECTION_ROUNDS', value: reportAgentMaxReflectionRounds } { name: 'REPORT_AGENT_TEMPERATURE', value: reportAgentTemperature } { name: 'FLASK_DEBUG', value: 'False' } // Storage: si s'usa Azure Files, les dades OASIS es guarden al volum muntat { name: 'OASIS_SIMULATION_DATA_DIR', value: empty(storageAccountName) ? '/app/backend/uploads/simulations' : '/mnt/uploads/simulations' } { name: 'UPLOAD_FOLDER', value: empty(storageAccountName) ? '/app/backend/uploads' : '/mnt/uploads' } { name: 'STORAGE_TYPE', value: empty(storageConnectionString) ? 'local' : 'azure' } ] var optionalEnv = concat( empty(llmBoostApiKey) ? [] : [{ name: 'LLM_BOOST_API_KEY', secretRef: 'llm-boost-api-key' }], empty(llmEmbedApiKey) ? [] : [{ name: 'LLM_EMBED_API_KEY', secretRef: 'llm-embed-api-key' }], empty(llmSmallApiKey) ? [] : [{ name: 'LLM_SMALL_API_KEY', secretRef: 'llm-small-api-key' }], empty(zepApiKey) ? [] : [{ name: 'ZEP_API_KEY', secretRef: 'zep-api-key' }], empty(neo4jPassword) ? [] : [{ name: 'NEO4J_PASSWORD', secretRef: 'neo4j-password' }], empty(storageConnectionString) ? [] : [{ name: 'AZURE_STORAGE_CONNECTION_STRING', secretRef: 'storage-connection-string' }], empty(databaseUrl) ? [] : [{ name: 'DATABASE_URL', secretRef: 'database-url' }] ) var allEnv = concat(mandatoryEnv, optionalEnv) // ─── Container App ───────────────────────────────────────────────────────────── resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { name: projectName location: location properties: { managedEnvironmentId: containerAppsEnvId configuration: { secrets: allSecrets // Credencials del registre privat (ACR) registries: [ { server: acrLoginServer username: acrUsername passwordSecretRef: 'acr-password' } ] // Ingrés: port únic 5001 (Flask serveix frontend + API) ingress: { external: true // TODO (ops): canviar a false en entorn intranet CTTI targetPort: 5001 transport: 'http' // TODO (ops): afegir domini corporatiu quan estigui assignat // customDomains: [ // { name: 'mirofish.intranet.gencat.cat', certificateId: '...', bindingType: 'SniEnabled' } // ] } // Revisió única activa (zero-downtime via revision-based deployments) activeRevisionsMode: 'Single' } template: { containers: [ { name: projectName image: containerImage env: allEnv // Recursos mínim viable — escalar horitzontalment via rèpliques resources: { cpu: json('2') memory: '4Gi' } // Muntar Azure Files quan s'ha configurat storage volumeMounts: empty(storageAccountName) ? [] : [ { volumeName: 'uploads' mountPath: '/mnt/uploads' } ] } ] // Volum Azure Files (registrat a l'entorn via infra.bicep) volumes: empty(storageAccountName) ? [] : [ { name: 'uploads' storageType: 'AzureFile' storageName: 'uploads' mountOptions: 'nobrl,cache=strict,nosharesock,actimeo=30' } ] // Escalat: mínim 1 rèplica (evita cold start en PRO), màxim 10 scale: { minReplicas: 1 maxReplicas: 10 rules: [ { name: 'http-scaling' http: { metadata: { concurrentRequests: '20' } } } ] } } } } // ─── Outputs ────────────────────────────────────────────────────────────────── @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