From cb4c35888224eac3e62e7d412b2edf2107d8fd2e Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Wed, 18 Mar 2026 12:57:39 -0400 Subject: [PATCH 01/29] Analysis --- .DS_Store | Bin 6148 -> 8196 bytes Analysis/01-overview.md | 430 ++++++ Analysis/02-architecture-detailed.md | 1920 ++++++++++++++++++++++++++ Analysis/03-use_cases_examples.md | 666 +++++++++ Analysis/04-ai_lz_options.md | 1224 ++++++++++++++++ Analysis/05-mini-ailz.md | 1002 ++++++++++++++ personal-private-configurations.md | 141 ++ 7 files changed, 5383 insertions(+) create mode 100644 Analysis/01-overview.md create mode 100644 Analysis/02-architecture-detailed.md create mode 100644 Analysis/03-use_cases_examples.md create mode 100644 Analysis/04-ai_lz_options.md create mode 100644 Analysis/05-mini-ailz.md create mode 100644 personal-private-configurations.md diff --git a/.DS_Store b/.DS_Store index 11765c4c72c09c28e71ba2b46a950db03450f6cf..18783b35c1162015272d62740bd922d6f9247c96 100644 GIT binary patch literal 8196 zcmeHMOKTHR6h1eJHQ=Jvg%lS$Kfni7yHlE$2)1s7q6mT{X<{49izGE^K^Sl)bfbtn zm9BKHvlDj7=EpYpbBR zr>-7^feKS%2n|QSC*5Ha#`@Z7I0+3WVIm7tp$HWnI8V8gNNDTMP64MtPyt!HkI`XD z(>kpN>-T+cy8DaeWHM7IXK+3K@y`Bc`tj@8pig$7Pi2Mg6R7Qi7n0X$2Z&cyyw-n?VoG;rfI1 z>6DJqb)RCn!zA2`^C9});&YX1v_xgf26j~D4Ik#g`S|B<9%FnguMVGZFPo2go5pFD zN|+%9S_7**)hvagCw_rZz=v~s6u z&-q+z-fn4R-Gh%!MGiXE=`xfmQ-cnHUxo6PT9NtRU^W23xjwyi zzr}SRs-xuaD4q{RQy|xeT>+zSKOfVVJ`6_id=7kRww{;pd{i7h8pETkd`_x)5+0|4 z5wr8zy*b`DABEug9Dg-af2p7U0daUf!=tQx_NG9N_Jq#22IgIrt{|(bHS5IJ(y%z5 z+viUYTXS!ym^{N#u~wcDU*@?3v#o@usY+=(n~iUFFXudQCpRhU|5M@n|65rCmzz^y+bbYq$;IRxWH7aLKoD7LdpJ@!WRrfrwhDriD#u|} zIS$+YharA@aFv*ZvA(vrg7(ih0{HitTmSWUD*JBWYw4(KIA3K)#oFtyFQ*1uzuo#T I1^Jup4?b@#^#A|> literal 6148 zcmeHKPfrs;6n_I1wji=Vi%2xt*oz4ei=dGhLoFCZ4-l3h2w3;ew5%*M&F+?pgrpxp z{Q!OdKZEh)#iLgbUi< zSQDcqd^Olj-LjWGS}*CD8O|chV>E~ZJeQ)&`U8)-yk6ur=7w>sDJQhFmM-bTdwXN! z`Ri9ljfwo!{;07xbtOM)OioxB-=S%v4Z8=+&KvXcpeI6A38eQ=m z&P?waU5V(^NGK(}|0lmi>OML!E$vp(Cv#wkbKMCjWowndbLY$DkVY$q^wpBy8#)2^ z{RJL+k;mEUdWhuDMRa?Pl{v1sgT&8Q#Zad*SFZ0gUmB99`z zEj&dwEMy^|B2&D!S+yLGRdUl`CAqo6LQ}aU6&-R?!3`zT)D)83JGT+*ON=v`PUi~u z)szM!a0~8%37dH1y@Xfr8s5Nr_yk|z8%dEta*>RXNph1+le=V&JR}a;Bp!Jp6?&5XEOiEU1gKgorT|78Eul;tUFrp@=e+s3QiE;b=FMUqN9*q6`P34j)9VOwywH?Qb*oud+#e#E&P!J0W8xqlil6(lnG$dWZz+YwHJMh!otN;K2 diff --git a/Analysis/01-overview.md b/Analysis/01-overview.md new file mode 100644 index 0000000..9a13436 --- /dev/null +++ b/Analysis/01-overview.md @@ -0,0 +1,430 @@ +# ContentFlow — Guía General de la Solución + +> **Plataforma inteligente de procesamiento de documentos y contenido construida sobre Azure.** + +--- + +## Índice + +- [1. ¿Qué es ContentFlow?](#1-qué-es-contentflow) +- [2. ¿Qué problema resuelve?](#2-qué-problema-resuelve) +- [3. Conceptos clave](#3-conceptos-clave) + - [3.1 Pipelines](#31-pipelines) + - [3.2 Executors (Ejecutores)](#32-executors-ejecutores) + - [3.3 Vaults (Bóvedas de documentos)](#33-vaults-bóvedas-de-documentos) + - [3.4 Modelo de Contenido](#34-modelo-de-contenido) +- [4. Componentes principales](#4-componentes-principales) + - [4.1 Interfaz Web (contentflow-web)](#41-interfaz-web-contentflow-web) + - [4.2 API REST (contentflow-api)](#42-api-rest-contentflow-api) + - [4.3 Workers de procesamiento (contentflow-worker)](#43-workers-de-procesamiento-contentflow-worker) + - [4.4 Librería de procesamiento (contentflow-lib)](#44-librería-de-procesamiento-contentflow-lib) + - [4.5 Infraestructura (infra)](#45-infraestructura-infra) +- [5. ¿Cómo funciona el flujo completo?](#5-cómo-funciona-el-flujo-completo) + - [5.1 Paso a paso: De un documento a datos inteligentes](#51-paso-a-paso-de-un-documento-a-datos-inteligentes) + - [5.2 Diagrama de flujo general](#52-diagrama-de-flujo-general) +- [6. Servicios de Azure utilizados](#6-servicios-de-azure-utilizados) +- [7. Modos de despliegue](#7-modos-de-despliegue) + - [7.1 Modo Básico](#71-modo-básico) + - [7.2 Modo AILZ (AI Landing Zone)](#72-modo-ailz-ai-landing-zone) +- [8. Ejemplo práctico: Pipeline de documentos PDF](#8-ejemplo-práctico-pipeline-de-documentos-pdf) +- [9. Resumen ejecutivo](#9-resumen-ejecutivo) + +--- + +## 1. ¿Qué es ContentFlow? + +**ContentFlow** es una plataforma empresarial cloud-native diseñada para transformar contenido no estructurado (PDFs, documentos Word, hojas de Excel, presentaciones PowerPoint, páginas web, etc.) en **datos inteligentes y accionables** mediante flujos de trabajo automatizados impulsados por servicios de inteligencia artificial de Azure. + +Piensa en ContentFlow como una **fábrica de procesamiento de documentos**: introduces documentos crudos por un extremo y, tras pasar por una serie de estaciones de procesamiento configurables (extracción, análisis, enriquecimiento, etc.), obtienes datos estructurados, indexados y listos para consumir. + +--- + +## 2. ¿Qué problema resuelve? + +Las organizaciones enfrentan desafíos comunes con su contenido: + +| Desafío | Cómo lo resuelve ContentFlow | +|---------|------------------------------| +| Documentos en múltiples formatos (PDF, Word, Excel...) | **Ejecutores de extracción** especializados para cada formato | +| Procesamiento manual y lento | **Pipelines automatizados** que procesan contenido sin intervención | +| Necesidad de análisis inteligente | **Integración con Azure AI** (GPT-4, Document Intelligence, embeddings) | +| Escalabilidad limitada | **Arquitectura distribuida** con workers paralelos y colas de mensajes | +| Dificultad para configurar flujos | **Editor visual** drag-and-drop + definición YAML | +| Seguridad empresarial | **Identidad administrada** (Managed Identity) sin contraseñas | + +--- + +## 3. Conceptos clave + +### 3.1 Pipelines + +Un **pipeline** es la unidad fundamental de trabajo en ContentFlow. Define una secuencia de pasos (ejecutores) que el contenido debe recorrer para ser procesado. Se definen en formato YAML: + +```yaml +name: procesar-pdfs +steps: + - executor: azure_blob_input_discovery # Descubre documentos + settings: + blob_container_name: documentos + + - executor: pdf_extractor # Extrae texto del PDF + + - executor: recursive_text_chunker # Divide en fragmentos + settings: + chunk_size: 1000 + + - executor: azure_openai_embeddings # Genera vectores (embeddings) + settings: + model: text-embedding-3-small + + - executor: azure_blob_output # Guarda resultados + settings: + blob_container_name: procesados +``` + +### 3.2 Executors (Ejecutores) + +Los **ejecutores** son las unidades de procesamiento individuales dentro de un pipeline. Cada ejecutor realiza una tarea específica. ContentFlow incluye **más de 35 ejecutores** organizados en categorías: + +| Categoría | Descripción | Ejemplos | +|-----------|-------------|----------| +| **Entrada** | Descubren contenido desde orígenes de datos | Blob Storage, sistema de archivos | +| **Extracción** | Parsean documentos y extraen texto | PDF, Word, Excel, PowerPoint, CSV | +| **Procesamiento IA** | Análisis inteligente con Azure AI | Embeddings, Document Intelligence, GPT-4 | +| **Transformación** | Manipulan y enriquecen datos | Chunking, mapeo de campos, agregación | +| **Salida** | Almacenan resultados procesados | Blob Storage, Azure AI Search | +| **Enrutamiento** | Lógica condicional y paralelismo | Fan-out/Fan-in, sub-pipelines | +| **Análisis de texto** | Procesamiento de lenguaje natural | Resumen, entidades, sentimiento, PII, idioma | +| **Especializado** | Tareas de dominio específico | Web scraping, grafos de conocimiento | + +### 3.3 Vaults (Bóvedas de documentos) + +Un **vault** es un contenedor lógico que agrupa documentos y los asocia a un pipeline específico. Cuando el vault está habilitado, los workers automáticamente descubren nuevos documentos y los procesan según el pipeline asociado. + +``` +Vault "Facturas 2024" + ├── Pipeline asociado: "procesar-facturas" + ├── Tags: [facturas, contabilidad] + ├── Documentos descubiertos: 1,247 + └── Estado: Habilitado ✓ +``` + +### 3.4 Modelo de Contenido + +Todo el contenido que fluye por un pipeline utiliza un **modelo estandarizado** (`Content`) que acumula datos a medida que pasa por cada ejecutor: + +``` +Content + ├── id: identificador único + ├── source: origen del documento (ej. blob://container/archivo.pdf) + ├── content: texto extraído + ├── metadata: información adicional del documento + ├── chunks: fragmentos de texto + ├── embeddings: vectores numéricos para búsqueda semántica + └── analysis: resultados de análisis IA +``` + +--- + +## 4. Componentes principales + +ContentFlow se compone de **5 componentes principales** que trabajan de forma coordinada: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ContentFlow │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ │ +│ │ Web │──▶│ API │──▶│ Workers │──▶│ Librería │ │ +│ │ (React) │ │ (FastAPI)│ │ (Python) │ │ (Python) │ │ +│ └──────────┘ └────┬─────┘ └─────┬─────┘ └───────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────────────────────┐ │ +│ │ Infraestructura Azure │ │ +│ │ (Cosmos DB, Blob, Queue, │ │ +│ │ AI Services, Container │ │ +│ │ Apps, App Configuration) │ │ +│ └──────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4.1 Interfaz Web (contentflow-web) + +La **interfaz web** es una aplicación React moderna que permite a los usuarios interactuar visualmente con ContentFlow: + +- **Constructor visual de pipelines**: Editor drag-and-drop basado en ReactFlow para diseñar pipelines gráficamente +- **Editor YAML**: Editor de código Monaco con resaltado de sintaxis para definir pipelines +- **Gestión de Vaults**: Crear, editar y monitorear bóvedas de documentos +- **Galería de plantillas**: Pipelines pre-construidos listos para usar +- **Panel de ejecuciones**: Ver el estado y resultados del procesamiento en tiempo real + +**Tecnologías**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn/ui, ReactFlow, Monaco Editor + +### 4.2 API REST (contentflow-api) + +El **servicio API** es el punto central de coordinación. Recibe solicitudes de la interfaz web y orquesta las operaciones: + +- **Gestión de pipelines**: Crear, leer, actualizar, eliminar y ejecutar pipelines +- **Gestión de vaults**: Administrar bóvedas de documentos +- **Catálogo de ejecutores**: Explorar los ejecutores disponibles y sus configuraciones +- **Verificación de salud**: Monitorear el estado de todos los servicios dependientes + +**Tecnologías**: FastAPI, Python 3.12+, Uvicorn, Pydantic, Azure SDK + +**Puerto**: 8090 + +### 4.3 Workers de procesamiento (contentflow-worker) + +Los **workers** son el motor de ejecución distribuida. Procesan el contenido de forma asíncrona utilizando múltiples procesos: + +- **Workers de entrada (Source Workers)**: Descubren contenido nuevo en los orígenes de datos configurados +- **Workers de procesamiento (Processing Workers)**: Ejecutan los pipelines sobre cada elemento de contenido + +**Funcionamiento simplificado**: +``` +Worker de Entrada Worker de Procesamiento + │ │ + ├── Lee vaults habilitados ├── Consulta la cola + ├── Ejecuta ejecutores de entrada ├── Recibe tarea + ├── Descubre documentos nuevos ├── Lee el pipeline + └── Crea tareas en la cola └── Ejecuta pipeline + sobre el contenido +``` + +**Tecnologías**: Python 3.12+, multiprocessing, Azure Storage Queue + +**Puerto**: 8099 (API de salud) + +### 4.4 Librería de procesamiento (contentflow-lib) + +La **librería** es el corazón técnico de ContentFlow. Contiene: + +- **Motor de pipelines**: Parsea YAML y ejecuta grafos dirigidos acíclicos (DAG) +- **+35 ejecutores**: Implementaciones listas para usar +- **Modelo de contenido**: Estructura de datos estandarizada +- **Conectores Azure**: Clientes para Blob, Cosmos DB, AI Search, OpenAI, Document Intelligence + +Esta librería se instala como dependencia en el API y el Worker. + +### 4.5 Infraestructura (infra) + +Plantillas **Bicep** (Infrastructure as Code) para aprovisionar todos los recursos de Azure necesarios. Se despliega con un solo comando `azd up`. + +--- + +## 5. ¿Cómo funciona el flujo completo? + +### 5.1 Paso a paso: De un documento a datos inteligentes + +``` + ① USUARIO ② API ③ COLA + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ Crea o edita│ ─────────▶ │ Almacena en │ ─────────▶ │ Encola │ + │ un pipeline │ REST API │ Cosmos DB y │ Storage │ tarea de │ + │ y ejecuta │ │ crea tarea │ Queue │ descubrir │ + └─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ⑥ RESULTADOS ⑤ PROCESAMIENTO ④ DESCUBRIMIENTO + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ Datos en │ ◀───────── │ Ejecuta cada│ ◀───────── │ Worker lee │ + │ Blob Storage│ Guardado │ ejecutor del│ Cola de │ el origen y │ + │ y Cosmos DB │ │ pipeline │ tareas │ lista docs │ + └─────────────┘ └─────────────┘ └─────────────┘ +``` + +**Flujo detallado**: + +1. **El usuario** crea un pipeline (visualmente o en YAML) y lo ejecuta desde la interfaz web +2. **La API** recibe la solicitud, guarda la configuración en Cosmos DB y crea un registro de ejecución +3. **La API** envía una tarea de tipo `InputSource` a la cola de Azure Storage +4. **El Worker de Entrada** lee la tarea, ejecuta los ejecutores de entrada del pipeline y descubre los documentos disponibles +5. **Por cada documento** descubierto, el Worker de Entrada crea una tarea `ContentProcessing` en la cola +6. **Los Workers de Procesamiento** toman las tareas de la cola y ejecutan el pipeline completo (extracción → transformación → análisis → salida) sobre cada documento +7. **Los resultados** se almacenan en Blob Storage y/o Cosmos DB +8. **El usuario** puede monitorear el progreso y ver los resultados desde la interfaz web + +### 5.2 Diagrama de flujo general + +``` +┌─────────────┐ HTTP/REST ┌─────────────────┐ +│ │ ──────────────────▶ │ │ +│ Frontend │ │ API Service │ +│ (React) │ ◀────────────────── │ (FastAPI) │ +│ │ Respuestas │ │ +└─────────────┘ └───────┬─────────┘ + │ + ┌───────────────┼──────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌────────────┐ ┌────────────┐ ┌──────────────┐ + │ Cosmos DB │ │ Blob │ │ Storage │ + │ (Metadatos │ │ Storage │ │ Queue │ + │ y estado) │ │(Documentos)│ │ (Tareas) │ + └──────┬─────┘ └─────┬──────┘ └───────┬──────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────────────────────────────────────┐ + │ Worker Service │ + │ ┌─────────────────┐ ┌──────────────────────┐ │ + │ │ Source Workers │ │ Processing Workers │ │ + │ │ (Descubrimiento) │ │ (Ejecución pipeline) │ │ + │ └────────┬────────┘ └──────────┬───────────┘ │ + │ │ │ │ + │ ▼ ▼ │ + │ ┌────────────────────────────────────────────┐ │ + │ │ contentflow-lib │ │ + │ │ (Motor de pipelines + 35+ Ejecutores) │ │ + │ └────────────────────┬───────────────────────┘ │ + └───────────────────────┼─────────────────────────┘ + │ + ▼ + ┌──────────────────────────┐ + │ Azure AI Services │ + │ ┌──────┐ ┌──────────┐ │ + │ │GPT-4 │ │ Document │ │ + │ │OpenAI│ │ Intelli- │ │ + │ │ │ │ gence │ │ + │ └──────┘ └──────────┘ │ + └──────────────────────────┘ +``` + +--- + +## 6. Servicios de Azure utilizados + +| Servicio de Azure | Propósito en ContentFlow | SKU / Nivel | +|-------------------|--------------------------|-------------| +| **Azure Container Apps** | Hospeda los 3 servicios (API, Worker, Web) | Consumption (serverless) | +| **Azure Cosmos DB** | Base de datos para metadatos, pipelines, ejecuciones | Serverless | +| **Azure Blob Storage** | Almacena documentos y contenido procesado | Standard_LRS, Hot | +| **Azure Storage Queue** | Cola de mensajes para distribución de tareas | Incluido en Storage | +| **Azure App Configuration** | Configuración centralizada de todos los servicios | Standard | +| **Azure Container Registry** | Registro de imágenes Docker | Standard (Premium con endpoints privados) | +| **Azure Log Analytics** | Workspace de logs y telemetría | PerGB2018 | +| **Azure Application Insights** | Monitoreo y diagnósticos | Conectado a Log Analytics | +| **Azure AI Foundry** | Plataforma de modelos de IA | S0 | +| **GPT-4.1 / GPT-4.1-mini** | Modelos de lenguaje para análisis inteligente | GlobalStandard, Capacidad: 100 | +| **Azure Document Intelligence** | Extracción inteligente de documentos (OCR) | Según integración | +| **Managed Identity** | Autenticación sin contraseñas entre servicios | User-Assigned | + +--- + +## 7. Modos de despliegue + +ContentFlow soporta dos modos de despliegue para adaptarse a diferentes necesidades: + +### 7.1 Modo Básico + +Ideal para **desarrollo, pruebas y demos**: + +- Endpoints públicos accesibles desde internet +- Red virtual creada automáticamente +- Todos los recursos se crean nuevos +- Sin endpoints privados +- Despliegue rápido y sencillo + +```bash +DEPLOYMENT_MODE=basic +azd up +``` + +### 7.2 Modo AILZ (AI Landing Zone) + +Diseñado para **entornos de producción empresarial**: + +- Se integra con una Azure AI Landing Zone existente +- Endpoints privados para todos los servicios +- Red virtual compartida con subnets dedicadas +- Zonas DNS privadas para resolución interna +- Cumplimiento con estándares de seguridad empresarial + +```bash +DEPLOYMENT_MODE=ailz-integrated +azd up +``` + +``` +┌─────────────────────────────────────────────────────────┐ +│ AI Landing Zone VNet │ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ aca-env-subnet │ │ pe-subnet │ │ +│ │ ┌────────────┐ │ │ ┌──────┐ ┌──────────┐ │ │ +│ │ │ Container │ │ │ │Cosmos│ │ Storage │ │ │ +│ │ │ Apps (API, │ │ │ │ DB │ │ Account │ │ │ +│ │ │ Worker,Web)│ │ │ │(PE) │ │ (PE) │ │ │ +│ │ └────────────┘ │ │ └──────┘ └──────────┘ │ │ +│ └──────────────────┘ └──────────────────────────┘ │ +│ │ +│ PE = Private Endpoint │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 8. Ejemplo práctico: Pipeline de documentos PDF + +A continuación, un ejemplo concreto de cómo ContentFlow procesa un conjunto de documentos PDF para crear un índice de búsqueda inteligente: + +**Pipeline: "Indexación de documentos PDF"** + +```yaml +name: indexacion-pdf +steps: + # 1. Descubrir PDFs en Blob Storage + - executor: azure_blob_input_discovery + settings: + blob_container_name: documentos-legales + file_extensions: [".pdf"] + + # 2. Recuperar contenido del blob + - executor: azure_blob_content_retriever + + # 3. Extraer texto con Azure Document Intelligence + - executor: azure_document_intelligence_extractor + settings: + model_id: prebuilt-layout + + # 4. Dividir en fragmentos manejables + - executor: recursive_text_chunker + settings: + chunk_size: 1000 + chunk_overlap: 200 + + # 5. Generar vectores para búsqueda semántica + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + # 6. Indexar en Azure AI Search + - executor: ai_search_index_output + settings: + index_name: documentos-legales-index +``` + +**Resultado**: Cada PDF se convierte en fragmentos indexados con vectores semánticos, listos para búsquedas inteligentes tipo "¿cuáles son las cláusulas de rescisión?". + +--- + +## 9. Resumen ejecutivo + +| Aspecto | Detalle | +|---------|---------| +| **Tipo de solución** | Plataforma de procesamiento de contenido empresarial | +| **Arquitectura** | Microservicios, event-driven, cloud-native | +| **Nube** | Microsoft Azure | +| **Servicios** | 3 contenedores: API, Worker, Web | +| **Motor de IA** | GPT-4.1, Document Intelligence, Embeddings | +| **Base de datos** | Cosmos DB (Serverless) | +| **Almacenamiento** | Azure Blob Storage | +| **Mensajería** | Azure Storage Queue | +| **Ejecutores disponibles** | 35+ ejecutores pre-construidos | +| **Formatos soportados** | PDF, Word, Excel, PowerPoint, CSV, HTML, web | +| **Seguridad** | Managed Identity, RBAC, sin contraseñas | +| **Despliegue** | `azd up` — un solo comando | +| **Modos** | Básico (desarrollo) o AILZ (producción empresarial) | + +--- + +> **ContentFlow** simplifica el camino desde documentos crudos hasta datos inteligentes, combinando la potencia de Azure AI con una arquitectura escalable y una experiencia de usuario intuitiva. diff --git a/Analysis/02-architecture-detailed.md b/Analysis/02-architecture-detailed.md new file mode 100644 index 0000000..a3f9f6e --- /dev/null +++ b/Analysis/02-architecture-detailed.md @@ -0,0 +1,1920 @@ +# ContentFlow — Arquitectura Detallada de Componentes + +> **Documentación técnica completa**: componentes, interconexiones, flujos de datos, SKUs, configuración y diagramas de la plataforma ContentFlow. + +--- + +## Índice + +- [1. Visión general de la arquitectura](#1-visión-general-de-la-arquitectura) + - [1.1 Diagrama de arquitectura completa](#11-diagrama-de-arquitectura-completa) + - [1.2 Principios arquitectónicos](#12-principios-arquitectónicos) +- [2. Componente: Interfaz Web (contentflow-web)](#2-componente-interfaz-web-contentflow-web) + - [2.1 Stack tecnológico](#21-stack-tecnológico) + - [2.2 Estructura de la aplicación](#22-estructura-de-la-aplicación) + - [2.3 Rutas y navegación](#23-rutas-y-navegación) + - [2.4 Componentes principales de la UI](#24-componentes-principales-de-la-ui) + - [2.5 Gestión de estado y datos](#25-gestión-de-estado-y-datos) + - [2.6 Diagrama de componentes Web](#26-diagrama-de-componentes-web) +- [3. Componente: API REST (contentflow-api)](#3-componente-api-rest-contentflow-api) + - [3.1 Stack tecnológico](#31-stack-tecnológico) + - [3.2 Endpoints del API](#32-endpoints-del-api) + - [3.3 Capa de servicios](#33-capa-de-servicios) + - [3.4 Capa de base de datos](#34-capa-de-base-de-datos) + - [3.5 Modelos de datos](#35-modelos-de-datos) + - [3.6 Inyección de dependencias](#36-inyección-de-dependencias) + - [3.7 Configuración y arranque](#37-configuración-y-arranque) + - [3.8 Diagrama de capas del API](#38-diagrama-de-capas-del-api) +- [4. Componente: Worker Service (contentflow-worker)](#4-componente-worker-service-contentflow-worker) + - [4.1 Stack tecnológico](#41-stack-tecnológico) + - [4.2 Arquitectura multi-proceso](#42-arquitectura-multi-proceso) + - [4.3 Worker de entrada (Input Source Worker)](#43-worker-de-entrada-input-source-worker) + - [4.4 Worker de procesamiento (Content Processing Worker)](#44-worker-de-procesamiento-content-processing-worker) + - [4.5 Cliente de cola](#45-cliente-de-cola) + - [4.6 Ciclo de vida de una tarea](#46-ciclo-de-vida-de-una-tarea) + - [4.7 Configuración del Worker](#47-configuración-del-worker) + - [4.8 Diagrama del motor de workers](#48-diagrama-del-motor-de-workers) +- [5. Componente: Librería Core (contentflow-lib)](#5-componente-librería-core-contentflow-lib) + - [5.1 Motor de pipelines](#51-motor-de-pipelines) + - [5.2 Jerarquía de clases de ejecutores](#52-jerarquía-de-clases-de-ejecutores) + - [5.3 Catálogo completo de ejecutores](#53-catálogo-completo-de-ejecutores) + - [5.4 Modelo de contenido (Content)](#54-modelo-de-contenido-content) + - [5.5 Conectores de Azure](#55-conectores-de-azure) + - [5.6 Diagrama del motor de pipelines](#56-diagrama-del-motor-de-pipelines) +- [6. Infraestructura de Azure (infra)](#6-infraestructura-de-azure-infra) + - [6.1 Recursos desplegados](#61-recursos-desplegados) + - [6.2 SKUs y niveles de servicio](#62-skus-y-niveles-de-servicio) + - [6.3 Módulos Bicep](#63-módulos-bicep) + - [6.4 Cosmos DB — Estructura de base de datos](#64-cosmos-db--estructura-de-base-de-datos) + - [6.5 Storage Account — Blobs y colas](#65-storage-account--blobs-y-colas) + - [6.6 Container Apps — Configuración de servicios](#66-container-apps--configuración-de-servicios) + - [6.7 AI Foundry — Modelos de IA](#67-ai-foundry--modelos-de-ia) + - [6.8 Diagrama de infraestructura Azure](#68-diagrama-de-infraestructura-azure) +- [7. Interconexión entre componentes](#7-interconexión-entre-componentes) + - [7.1 Flujo de datos completo](#71-flujo-de-datos-completo) + - [7.2 Flujo de mensajes por cola](#72-flujo-de-mensajes-por-cola) + - [7.3 Flujo de autenticación](#73-flujo-de-autenticación) + - [7.4 Matriz de comunicación entre servicios](#74-matriz-de-comunicación-entre-servicios) + - [7.5 Diagrama de secuencia: Ejecución de pipeline](#75-diagrama-de-secuencia-ejecución-de-pipeline) +- [8. Modos de despliegue](#8-modos-de-despliegue) + - [8.1 Modo básico](#81-modo-básico) + - [8.2 Modo AILZ (AI Landing Zone)](#82-modo-ailz-ai-landing-zone) + - [8.3 Proceso de despliegue con azd](#83-proceso-de-despliegue-con-azd) + - [8.4 Diagrama comparativo de modos](#84-diagrama-comparativo-de-modos) +- [9. Seguridad y autenticación](#9-seguridad-y-autenticación) + - [9.1 Modelo de identidad](#91-modelo-de-identidad) + - [9.2 Roles RBAC asignados](#92-roles-rbac-asignados) + - [9.3 Diagrama de seguridad](#93-diagrama-de-seguridad) +- [10. Configuración centralizada](#10-configuración-centralizada) + - [10.1 Azure App Configuration](#101-azure-app-configuration) + - [10.2 Variables de entorno por servicio](#102-variables-de-entorno-por-servicio) +- [11. Monitoreo y observabilidad](#11-monitoreo-y-observabilidad) +- [12. Resumen de dependencias entre componentes](#12-resumen-de-dependencias-entre-componentes) + +--- + +## 1. Visión general de la arquitectura + +ContentFlow implementa una **arquitectura de microservicios event-driven (basada en eventos)** con los siguientes patrones clave: + +- **Microservicios**: 3 servicios independientes (API, Worker, Web) desplegados como contenedores +- **Event-Driven**: Comunicación asíncrona mediante colas de mensajes +- **Cloud-Native**: Diseñado para Azure Container Apps con escalado automático +- **Declarativo**: Pipelines definidos en YAML, infraestructura definida en Bicep + +### 1.1 Diagrama de arquitectura completa + +``` +┌──────────────────────────────────────────────────────────────────────────────────┐ +│ AZURE CONTAINER APPS ENVIRONMENT │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ contentflow-web │ │ contentflow-api │ │contentflow-worker│ │ +│ │ │ │ │ │ │ │ +│ │ React 18 │ │ FastAPI │ │ Multi-process │ │ +│ │ TypeScript │───▶│ Python 3.12+ │ │ Python 3.12+ │ │ +│ │ Vite │ │ Puerto: 8090 │ │ Puerto: 8099 │ │ +│ │ ReactFlow │ │ │ │ │ │ +│ │ Monaco Editor │ │ ┌────────────┐ │ │ ┌──────────────┐ │ │ +│ │ │ │ │ Routers │ │ │ │Source Workers│ │ │ +│ │ │ │ │ Services │ │ │ │Process Worker│ │ │ +│ │ │ │ │ Models │ │ │ │Queue Client │ │ │ +│ │ │ │ └────────────┘ │ │ └──────────────┘ │ │ +│ │ CPU: 1 | 2Gi │ │ CPU: 1 | 2Gi │ │ CPU: 1 | 2Gi │ │ +│ │ Réplicas: 1-2 │ │ Réplicas: 1-2 │ │ Réplicas: 1-2 │ │ +│ └──────────────────┘ └───────┬──────────┘ └────────┬─────────┘ │ +│ │ │ │ +└──────────────────────────────────┼─────────────────────────┼─────────────────────┘ + │ │ + ┌───────────────────────┼─────────────────────────┼──────────────┐ + │ ▼ ▼ │ + │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │ + │ │ Cosmos DB │ │ Blob │ │ Storage Queue │ │ + │ │ (Serverless)│ │ Storage │ │ (contentflow- │ │ + │ │ │ │ (Standard │ │ execution-requests) │ │ + │ │ 7 containers│ │ LRS, Hot) │ │ │ │ + │ └─────────────┘ └─────────────┘ └──────────────────────┘ │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │ + │ │ App Config │ │ App Insights │ │ AI Foundry (S0) │ │ + │ │ (Standard) │ │ + Log Analyt.│ │ GPT-4.1 │ │ + │ │ │ │ (PerGB2018) │ │ GPT-4.1-mini │ │ + │ └──────────────┘ └──────────────┘ └────────────────────┘ │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ │ + │ │ Container │ │ Managed │ │ + │ │ Registry │ │ Identity │ │ + │ │ (Standard) │ │ (User Assign)│ │ + │ └──────────────┘ └──────────────┘ │ + │ │ + │ SERVICIOS DE AZURE │ + └────────────────────────────────────────────────────────────────┘ +``` + +### 1.2 Principios arquitectónicos + +| Principio | Implementación | +|-----------|---------------| +| **Desacoplamiento** | Los servicios se comunican a través de colas y base de datos compartida, no llamadas directas | +| **Escalabilidad horizontal** | Workers pueden escalarse independientemente (1-2 réplicas con autoescalado) | +| **Procesamiento asíncrono** | Las tareas se encolan y procesan en segundo plano | +| **Resiliencia** | Reintentos configurables (3 por defecto), timeouts, y manejo de errores por ejecutor | +| **Seguridad Zero-Trust** | Managed Identity, sin contraseñas compartidas, RBAC para cada recurso | +| **Configuración centralizada** | Azure App Configuration como fuente única de verdad | +| **Observabilidad** | Application Insights + Log Analytics para trazabilidad distribuida | + +--- + +## 2. Componente: Interfaz Web (contentflow-web) + +### 2.1 Stack tecnológico + +| Tecnología | Versión | Rol | +|------------|---------|-----| +| React | 18.3.1 | Framework UI | +| TypeScript | 5.8.3 | Lenguaje tipado | +| Vite | 7.1.5 | Empaquetador y servidor de desarrollo | +| Tailwind CSS | 3.4.17 | Framework de estilos utilitarios | +| ReactFlow | 11.11.4 | Visualización de grafos (pipeline builder) | +| Monaco Editor | 4.7.0 | Editor de código YAML | +| TanStack Query | 5.83.0 | Gestión de estado del servidor | +| React Hook Form | 7.61.1 | Formularios con validación | +| Zod | 3.25.76 | Esquemas de validación TypeScript | +| Shadcn/ui + Radix UI | Múltiples | Componentes UI accesibles | +| Recharts | 2.15.4 | Gráficas y visualizaciones | +| Lucide React | 0.462.0 | Iconografía | +| js-yaml | 4.1.1 | Parsing YAML | +| date-fns | 3.6.0 | Utilidades de fecha | + +### 2.2 Estructura de la aplicación + +``` +contentflow-web/src/ +├── main.tsx # Punto de entrada de React +├── App.tsx # Componente raíz con providers y rutas +├── index.css # Estilos globales (Tailwind) +├── components/ +│ ├── ui/ # Componentes base Shadcn/ui +│ │ ├── button.tsx # Botones +│ │ ├── dialog.tsx # Diálogos modales +│ │ ├── input.tsx # Campos de entrada +│ │ ├── select.tsx # Selectores +│ │ ├── toast.tsx # Notificaciones +│ │ ├── tabs.tsx # Pestañas +│ │ ├── card.tsx # Tarjetas +│ │ └── ... # +20 componentes más +│ ├── pipeline/ # Componentes del constructor de pipelines +│ ├── vaults/ # Componentes de gestión de vaults +│ ├── dashboard/ # Componentes del panel principal +│ ├── knowledge/ # Componentes del grafo de conocimiento +│ ├── templates/ # Componentes de plantillas +│ ├── Hero.tsx # Sección de bienvenida +│ ├── PipelineBuilder.tsx # Constructor visual de pipelines +│ ├── KnowledgeGraph.tsx # Visualizador de grafos +│ ├── Footer.tsx # Pie de página +│ └── NavLink.tsx # Enlace de navegación +├── pages/ +│ ├── Index.tsx # Página principal (home, pipeline, vaults) +│ ├── Templates.tsx # Galería de plantillas +│ ├── Vaults.tsx # Gestión de vaults (ruta directa) +│ └── NotFound.tsx # Página 404 +├── hooks/ # Custom hooks de React +├── types/ # Definiciones de tipos TypeScript +├── lib/ # Utilidades y configuración (cn, utils) +└── data/ # Datos mock y constantes +``` + +### 2.3 Rutas y navegación + +| Ruta | Componente | Vista | Descripción | +|------|-----------|-------|-------------| +| `/` | Index | home | Página principal con hero y navegación | +| `/?view=pipeline` | Index | pipeline | Constructor visual de pipelines | +| `/?view=graph` | Index | graph | Visualizador de grafos de conocimiento | +| `/?view=vaults` | Index | vaults | Gestión de bóvedas de documentos | +| `/templates` | Templates | — | Galería de plantillas pre-construidas | +| `/*` | NotFound | — | Página 404 | + +### 2.4 Componentes principales de la UI + +``` +┌────────────────────────────────────────────────────────────────┐ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Barra de Navegación │ │ +│ │ [Logo] ContentFlow | Home | Pipeline | Vaults | │ │ +│ │ Templates │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ CONTENIDO PRINCIPAL (según vista activa) │ │ +│ │ │ │ +│ │ ┌─ Home ──────────────────────────────────────────┐ │ │ +│ │ │ Hero: Bienvenida y métricas del sistema │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─ Pipeline Builder ──────────────────────────────┐ │ │ +│ │ │ ┌───────────┐ ┌───────────────────────────┐ │ │ │ +│ │ │ │ Catálogo │ │ Lienzo ReactFlow │ │ │ │ +│ │ │ │ de │ │ (Nodos + Aristas) │ │ │ │ +│ │ │ │ ejecutores│ │ │ │ │ │ +│ │ │ │ │ │ [Nodo A] ──▶ [Nodo B] │ │ │ │ +│ │ │ │ • Input │ │ │ │ │ │ │ +│ │ │ │ • Extract │ │ ▼ │ │ │ │ +│ │ │ │ • Process │ │ [Nodo C] ──▶ [Nodo D] │ │ │ │ +│ │ │ │ • Output │ │ │ │ │ │ +│ │ │ └───────────┘ └───────────────────────────┘ │ │ │ +│ │ │ ┌─────────────────────────────────────────────┐│ │ │ +│ │ │ │ Editor YAML (Monaco) ││ │ │ +│ │ │ └─────────────────────────────────────────────┘│ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─ Vaults ────────────────────────────────────────┐ │ │ +│ │ │ Lista de bóvedas │ Detalle │ Ejecuciones │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Footer │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────┘ +``` + +### 2.5 Gestión de estado y datos + +| Librería | Responsabilidad | Patrón | +|----------|----------------|--------| +| **TanStack Query** | Estado del servidor (datos de la API) | Cache con invalidación automática | +| **React Hook Form** | Estado de formularios | Formularios controlados con validación | +| **Zod** | Validación de esquemas | Validación en tiempo de ejecución | +| **URL Query Params** | Navegación y vista activa | Estado en la URL (ej. `?view=pipeline`) | +| **React Context** | Estado global de la app (si aplica) | Provider pattern | + +### 2.6 Diagrama de componentes Web + +``` + App.tsx + │ + ┌───────────┼───────────────┐ + │ │ │ + QueryClient BrowserRouter Providers + Provider │ (Tooltip, + │ │ Toast, + │ ┌─────┴──────┐ Sonner) + │ │ │ + │ Route / Route /templates + │ │ │ + │ Index Templates + │ │ + │ ├── Hero + │ ├── PipelineBuilder ──▶ ReactFlow + Monaco + │ ├── KnowledgeGraph ──▶ Visualización de grafos + │ └── Vaults ──▶ CRUD de bóvedas + │ + └── Comunicación con API vía fetch/httpx + (GET/POST/PUT/DELETE → /api/*) +``` + +--- + +## 3. Componente: API REST (contentflow-api) + +### 3.1 Stack tecnológico + +| Tecnología | Versión | Rol | +|------------|---------|-----| +| Python | 3.12+ | Runtime | +| FastAPI | 0.128.0 | Framework web asíncrono | +| Uvicorn | 0.40.0 | Servidor ASGI | +| Pydantic | 2.12+ | Validación de datos y modelos | +| pydantic-settings | 2.12.0 | Gestión de configuración | +| azure-identity | 1.25.1 | Autenticación con Managed Identity | +| azure-cosmos | 4.14.3 | Cliente Cosmos DB NoSQL | +| azure-storage-blob | 12.27.1 | Cliente Blob Storage | +| azure-storage-queue | 12.14.1 | Cliente Storage Queue | +| azure-appconfiguration-provider | 2.3.1 | Configuración centralizada | +| httpx | 0.28.1 | Cliente HTTP asíncrono | +| aiohttp | 3.13.3+ | Cliente HTTP asíncrono alternativo | +| PyYAML | 6.0.3+ | Parsing YAML | +| python-dotenv | 1.2.1+ | Variables de entorno locales | +| contentflow-lib | local | Librería de procesamiento (editable) | + +### 3.2 Endpoints del API + +#### Salud del sistema + +| Método | Ruta | Descripción | Respuesta | +|--------|------|-------------|-----------| +| `GET` | `/api/health/` | Verificación completa de todos los servicios | `SystemHealth` | +| `GET` | `/api/health/{service}` | Verificar un servicio específico | `ServiceHealth` | + +Servicios verificados: `cosmos_db`, `storage_queue`, `app_config` + +#### Gestión de Pipelines + +| Método | Ruta | Descripción | Request Body | Respuesta | +|--------|------|-------------|-------------|-----------| +| `GET` | `/api/pipelines/` | Listar todos los pipelines | — | `List[Pipeline]` | +| `GET` | `/api/pipelines/{id_or_name}` | Obtener por ID o nombre | — | `Pipeline` | +| `POST` | `/api/pipelines/` | Crear o actualizar pipeline | `SavePipelineRequest` | `Pipeline` | +| `DELETE` | `/api/pipelines/{id}` | Eliminar pipeline | — | `bool` | +| `POST` | `/api/pipelines/{id}/execute` | Ejecutar pipeline | `ExecutePipelineRequest` | `ExecutePipelineResponse` | + +#### Gestión de Vaults + +| Método | Ruta | Descripción | Parámetros | Respuesta | +|--------|------|-------------|-----------|-----------| +| `GET` | `/api/vaults/` | Listar vaults | `search`, `tags` | `List[Vault]` | +| `GET` | `/api/vaults/{id}` | Obtener vault por ID | — | `Vault` | +| `POST` | `/api/vaults/` | Crear vault | Body: `VaultCreateRequest` | `Vault` | +| `PUT` | `/api/vaults/{id}` | Actualizar vault | Body: `VaultUpdateRequest` | `Vault` | +| `DELETE` | `/api/vaults/{id}` | Eliminar vault | — | `bool` | + +#### Catálogo de Ejecutores + +| Método | Ruta | Descripción | Respuesta | +|--------|------|-------------|-----------| +| `GET` | `/api/executors/` | Listar todos los ejecutores | `List[ExecutorCatalogDefinition]` | +| `GET` | `/api/executors/{id}` | Obtener ejecutor por ID | `ExecutorCatalogDefinition` | + +#### Raíz + +| Método | Ruta | Descripción | +|--------|------|-------------| +| `GET` | `/` | Mensaje de bienvenida y versión del API | + +### 3.3 Capa de servicios + +Cada servicio implementa lógica de negocio específica heredando de `BaseService`: + +``` +BaseService (CRUD genérico sobre Cosmos DB) + │ + ├── PipelineService + │ ├── get_pipelines() → List[Pipeline] + │ ├── get_pipeline_by_id(id) → Pipeline + │ ├── get_pipeline_by_name(name) → Pipeline + │ ├── create_pipeline(data) → Pipeline + │ ├── create_or_save_pipeline(data) → Pipeline + │ ├── update_by_id(id, data) → Pipeline + │ └── delete_pipeline_by_id(id) → bool + │ + ├── VaultService + │ ├── create_vault(request, pipeline_name) → Vault + │ ├── get_vault(id) → Vault + │ ├── list_vaults(search, tags) → List[Vault] + │ ├── update_vault(id, request) → Vault + │ └── delete_vault(id) → bool + │ + ├── VaultExecutionService + │ ├── get_vault_executions(vault_id, start, end) → List[VaultExecution] + │ ├── get_vault_crawl_checkpoints(vault_id) → List[Checkpoint] + │ ├── delete_vault_executions(vault_id) + │ └── delete_vault_crawl_checkpoints(vault_id) + │ + ├── PipelineExecutionService + │ └── Seguimiento de ejecuciones de pipeline + │ + ├── ExecutorCatalogService + │ └── Gestión del catálogo de ejecutores + │ + └── HealthService + ├── check_all_services() → SystemHealth + └── check_service_health(name) → ServiceHealth +``` + +### 3.4 Capa de base de datos + +**CosmosDBClient** — Wrapper asíncrono sobre el SDK oficial de Azure Cosmos DB: + +``` +CosmosDBClient + │ + ├── connect() # Inicializar conexión con credenciales + ├── close() # Cerrar conexión + │ + ├── create(container, item) # Crear documento + ├── get_by_id(container, id) # Obtener por ID + ├── list_all(container, query) # Listar con consulta SQL + ├── query(container, query, params) # Consulta parametrizada + ├── update(container, item) # Upsert (crear o actualizar) + ├── delete(container, id) # Eliminar + └── batch_upsert(container, items) # Actualización masiva +``` + +**Características**: +- Autenticación via `ChainedTokenCredential` (Managed Identity) +- Creación automática de la base de datos si no existe +- Consultas cross-partition habilitadas +- Pool de conexiones compartido + +### 3.5 Modelos de datos + +``` +CosmosBaseModel (Base) + │ + ├── id: str (UUID v4 auto-generado) + ├── created_at: datetime + └── modified_at: datetime + +Pipeline (Definición de pipeline) + ├── name: str + ├── description: str + ├── yaml: str (definición YAML del pipeline) + ├── nodes: List[Any] (nodos del grafo visual) + ├── edges: List[Any] (aristas del grafo visual) + ├── tags: List[str] + ├── enabled: bool + ├── retry_delay: int (segundos, default: 5) + ├── timeout: int (segundos, default: 600) + ├── retries: int (default: 3) + └── version: str + +PipelineExecution (Ejecución de pipeline) + ├── pipeline_id: str + ├── pipeline_name: str + ├── status: ExecutionStatus (pending|running|completed|failed|cancelled) + ├── inputs: Dict + ├── configuration: Dict + ├── outputs: Dict + ├── started_at: str (ISO) + ├── completed_at: str (ISO) + ├── duration_seconds: float + ├── error: str + ├── executor_outputs: Dict[str, ExecutorOutput] + └── events: List[PipelineExecutionEvent] + +Vault (Bóveda de documentos) + ├── name: str (1-100 caracteres) + ├── description: str (max 500) + ├── pipeline_id: str + ├── pipeline_name: str + ├── tags: List[str] + ├── save_execution_output: bool + ├── enabled: bool + └── created_by: str + +VaultExecution (Ejecución de vault) + ├── pipeline_id / vault_id: str + ├── status: pending|running|completed|failed + ├── task_id: str + ├── source_worker_id / processing_worker_id: str + ├── executor_outputs: Dict + ├── events: List[Dict] + ├── content: List[Dict] + └── number_of_items: int + +VaultCrawlCheckpoint (Punto de control de rastreo) + ├── pipeline_id / vault_id / executor_id: str + ├── checkpoint_timestamp: str + └── worker_id: str +``` + +### 3.6 Inyección de dependencias + +FastAPI utiliza inyección de dependencias para proporcionar clientes compartidos a los routers: + +``` +get_config_provider() ──▶ ConfigurationProvider (Azure App Config) + │ │ + ▼ ▼ +get_cosmos_client() ──▶ CosmosDBClient (singleton) + │ + ├──▶ get_pipeline_service() ──▶ PipelineService + ├──▶ get_vault_service() ──▶ VaultService + ├──▶ get_vault_execution_service() ──▶ VaultExecutionService + ├──▶ get_executor_catalog_service() ──▶ ExecutorCatalogService + ├──▶ get_pipeline_execution_service() ──▶ PipelineExecutionService + └──▶ get_health_service() ──▶ HealthService + +Todas las dependencias usan un TTL de caché de 10 minutos. +``` + +### 3.7 Configuración y arranque + +**Fuentes de configuración** (en orden de prioridad): + +1. **Variables de entorno** (ej. `.env` para desarrollo local) +2. **Azure App Configuration** (producción) +3. **Valores por defecto** (hardcoded en `settings.py`) + +**Secuencia de arranque del API**: + +``` +main.py + │ + ├── 1. Crear aplicación FastAPI + ├── 2. Configurar CORS (allow_origins: ["*"]) + ├── 3. Registrar routers (/api/health, /api/pipelines, /api/vaults, /api/executors) + │ + └── Lifespan (async context manager): + ├── STARTUP: + │ ├── initialize_cosmos() → Conectar a Cosmos DB + │ ├── initialize_blob_storage() → Inicializar Blob Storage + │ └── initialize_executor_catalog() → Cargar catálogo de ejecutores + │ + └── SHUTDOWN: + └── Limpieza de recursos +``` + +**Configuración del servidor**: + +| Parámetro | Valor | Descripción | +|-----------|-------|-------------| +| Host | `0.0.0.0` | Escuchar en todas las interfaces | +| Puerto | `8090` | Puerto HTTP | +| Workers | `1` | Un proceso Uvicorn | +| Reload | `True` (en DEBUG) | Recarga automática en desarrollo | +| OpenAPI docs | `/docs` | Documentación interactiva Swagger | +| ReDoc | `/redoc` | Documentación alternativa | + +### 3.8 Diagrama de capas del API + +``` +┌─────────────────────────────────────────────────────────┐ +│ CAPA DE PRESENTACIÓN │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ +│ │ /health │ │/pipelines│ │ /vaults │ │/executors │ │ +│ │ Router │ │ Router │ │ Router │ │ Router │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ +│ │ │ │ │ │ +├───────┼────────────┼────────────┼──────────────┼─────────┤ +│ ▼ ▼ ▼ ▼ │ +│ CAPA DE SERVICIOS │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ +│ │ Health │ │ Pipeline │ │ Vault │ │ Catalog │ │ +│ │ Service │ │ Service │ │ Service │ │ Service │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ +│ │ │ │ │ │ +├───────┼────────────┼────────────┼──────────────┼─────────┤ +│ ▼ ▼ ▼ ▼ │ +│ CAPA DE DATOS │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ CosmosDBClient (Async) │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ ChainedTokenCredential (Managed Identity) │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ +│ │ BlobStorage │ │ StorageQueue │ │ AppConfig │ │ +│ │ Client │ │ Client │ │ Provider │ │ +│ └──────────────┘ └──────────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. Componente: Worker Service (contentflow-worker) + +### 4.1 Stack tecnológico + +| Tecnología | Versión | Rol | +|------------|---------|-----| +| Python | 3.12+ | Runtime | +| multiprocessing | stdlib | Paralelismo de procesos | +| FastAPI | 0.128.0 | API de salud (opcional) | +| azure-storage-queue | 12.14.1 | Consumir tareas de la cola | +| azure-cosmos | 4.14.3 | Leer pipelines y actualizar estados | +| azure-storage-blob | 12.27.1 | Acceso a documentos | +| contentflow-lib | local | Motor de pipelines | + +### 4.2 Arquitectura multi-proceso + +El Worker utiliza el módulo `multiprocessing` de Python para ejecutar múltiples procesos independientes: + +``` +┌──────────────────────────────────────────────────────────┐ +│ WORKER ENGINE │ +│ │ +│ ┌────────────────────────────┐ │ +│ │ Proceso Principal │ │ +│ │ (main.py) │ │ +│ │ │ │ +│ │ ├── Signal Handler │ │ +│ │ ├── WorkerEngine.run() │ │ +│ │ └── API Thread (8099) │ │ +│ └─────────────┬──────────────┘ │ +│ │ │ +│ ┌───────────┴───────────────────────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌────────────────────────┐ ┌────────────────────────┐ │ +│ │ Source Worker(s) │ │ Processing Worker(s) │ │ +│ │ (InputSourceWorker) │ │ (ContentProcessing │ │ +│ │ │ │ Worker) │ │ +│ │ Cantidad: N_SOURCE │ │ Cantidad: N_PROCESS │ │ +│ │ Default: 1 │ │ Default: 0 │ │ +│ │ │ │ │ │ +│ │ Ciclo: │ │ Ciclo: │ │ +│ │ 1. Leer vaults │ │ 1. Poll cola │ │ +│ │ 2. Ejecutar inputs │ │ 2. Recibir tarea │ │ +│ │ 3. Descubrir contenido│ │ 3. Leer pipeline │ │ +│ │ 4. Crear tareas │ │ 4. Ejecutar pipeline │ │ +│ │ 5. Dormir (300s) │ │ 5. Actualizar estado │ │ +│ └────────────────────────┘ └────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ multiprocessing.Event (stop_event) │ │ +│ │ → Señal compartida para apagado coordinado │ │ +│ └───────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────┘ +``` + +### 4.3 Worker de entrada (Input Source Worker) + +**Responsabilidad**: Descubrir contenido nuevo en los orígenes de datos configurados. + +**Flujo de ejecución**: + +``` +┌─ Bucle principal ────────────────────────────────┐ +│ │ +│ 1. Consultar Cosmos DB → vaults habilitados │ +│ 2. Para cada vault: │ +│ a. Leer pipeline asociado │ +│ b. Obtener checkpoint más reciente │ +│ c. Ejecutar ejecutor(es) de input │ +│ d. Descubrir documentos nuevos │ +│ e. Por cada documento nuevo: │ +│ - Crear ContentProcessingTask │ +│ - Encolar en Storage Queue │ +│ f. Guardar nuevo checkpoint │ +│ 3. Dormir (DEFAULT_POLLING_INTERVAL_SECONDS) │ +│ 4. Repetir hasta stop_event │ +│ │ +└───────────────────────────────────────────────────┘ +``` + +**Gestión de checkpoints**: El sistema implementa **rastreo incremental** para evitar reprocesar documentos: +- En la primera ejecución, no hay checkpoint (se descubren todos los documentos) +- En ejecuciones posteriores, solo se descubren documentos nuevos o modificados desde el último checkpoint +- Los checkpoints se almacenan en el contenedor `vault_crawl_checkpoints` de Cosmos DB + +### 4.4 Worker de procesamiento (Content Processing Worker) + +**Responsabilidad**: Ejecutar pipelines completos sobre elementos de contenido individuales. + +**Flujo de ejecución**: + +``` +┌─ Bucle principal ────────────────────────────────┐ +│ │ +│ 1. Poll Azure Storage Queue │ +│ (max 32 mensajes, visibilidad 300s) │ +│ 2. Para cada mensaje: │ +│ a. Deserializar ContentProcessingTask │ +│ b. Obtener bloqueo distribuido │ +│ c. Leer pipeline de Cosmos DB │ +│ d. Crear PipelineExecutor │ +│ e. Ejecutar pipeline (sin ejecutores input) │ +│ f. Actualizar estado en Cosmos DB │ +│ g. Eliminar mensaje de la cola │ +│ 3. Si no hay mensajes → dormir (5s) │ +│ 4. Repetir hasta stop_event │ +│ │ +└───────────────────────────────────────────────────┘ +``` + +### 4.5 Cliente de cola + +**TaskQueueClient** — Abstracción sobre Azure Storage Queue: + +``` +TaskQueueClient + │ + ├── ensure_queue_exists() + │ → Crea la cola si no existe + │ + ├── send_content_processing_task(task: ContentProcessingTask) + │ → Serializa tarea → JSON → Base64 → Envía a cola + │ + ├── send_input_source_task(task: InputSourceTask) + │ → Serializa tarea → JSON → Base64 → Envía a cola + │ + └── _send_message(message, visibility_timeout) + → Envío interno con manejo de errores +``` + +**Formato del mensaje en cola**: +```json +{ + "task_type": "CONTENT_PROCESSING", + "payload": { + "pipeline_id": "abc-123", + "vault_id": "def-456", + "content": { ... } + } +} +``` + +### 4.6 Ciclo de vida de una tarea + +``` + ┌──────────┐ + │ PENDING │ ← Tarea creada y encolada + └────┬─────┘ + │ + ┌────▼─────┐ + │ RUNNING │ ← Worker toma la tarea + └────┬─────┘ + │ + ┌────────┼────────┐ + │ │ │ + ┌────▼───┐ ┌──▼───┐ ┌─▼──────┐ + │COMPLETED│ │FAILED│ │CANCELLED│ + └────────┘ └──────┘ └────────┘ + │ │ + │ ┌────▼────┐ + │ │ RETRY? │ (max 3 reintentos) + │ └────┬────┘ + │ │ Sí + │ ┌────▼─────┐ + │ │ PENDING │ + │ └──────────┘ + │ + Resultado almacenado + en Cosmos DB + Blob +``` + +### 4.7 Configuración del Worker + +| Parámetro | Default | Descripción | +|-----------|---------|-------------| +| `WORKER_NAME` | `contentflow-worker` | Nombre del worker | +| `NUM_PROCESSING_WORKERS` | `0` | Cantidad de workers de procesamiento | +| `NUM_SOURCE_WORKERS` | `1` | Cantidad de workers de entrada | +| `QUEUE_POLL_INTERVAL_SECONDS` | `5` | Intervalo de consulta a la cola | +| `QUEUE_VISIBILITY_TIMEOUT_SECONDS` | `300` | Timeout de visibilidad (5 min) | +| `QUEUE_MAX_MESSAGES` | `32` | Máximo de mensajes por poll | +| `DEFAULT_POLLING_INTERVAL_SECONDS` | `300` | Intervalo de descubrimiento (5 min) | +| `LOCK_TTL_SECONDS` | `300` | Tiempo de vida del bloqueo distribuido | +| `MAX_TASK_RETRIES` | `3` | Reintentos máximos por tarea | +| `TASK_TIMEOUT_SECONDS` | `600` | Timeout por tarea (10 min) | +| `API_ENABLED` | `true` | Habilitar API de salud | +| `API_PORT` | `8099` | Puerto del API de salud | + +### 4.8 Diagrama del motor de workers + +``` +┌─────────────────────────────────────────────────────────────┐ +│ WORKER ENGINE │ +│ │ +│ Señales del SO ──▶ Signal Handler ──▶ stop_event.set() │ +│ (SIGINT/SIGTERM) │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ WorkerEngine.run() │ │ +│ │ ├── start() │ │ +│ │ │ ├── Crear N source workers (mp.Process) │ │ +│ │ │ └── Crear N processing workers (mp.Process) │ │ +│ │ │ │ │ +│ │ └── monitor loop (mientras no stop_event): │ │ +│ │ ├── Verificar procesos activos │ │ +│ │ ├── Reemplazar workers caídos │ │ +│ │ └── sleep(1 segundo) │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ Apagado: │ +│ 1. stop_event.set() │ +│ 2. join(timeout=30s) por cada worker │ +│ 3. terminate() workers que no respondieron │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 5. Componente: Librería Core (contentflow-lib) + +### 5.1 Motor de pipelines + +El motor de pipelines es el cerebro de ContentFlow. Convierte definiciones YAML declarativas en grafos de ejecución: + +``` +YAML Pipeline Definition + │ + ▼ +┌─────────────────┐ +│ PipelineFactory │ ← Parsea YAML y crea instancias de ejecutores +│ │ +│ parse_yaml() │ +│ create_pipeline()│ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ DAG (Grafo │ ← Grafo dirigido acíclico de ejecutores +│ Dirigido │ +│ Acíclico) │ +│ │ +│ Nodos: Ejecutors│ +│ Aristas: Depend.│ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│PipelineExecutor │ ← Ejecuta el DAG resolviendo dependencias +│ │ +│ execute() │ Características: +│ │ • Ejecución asíncrona (asyncio) +│ │ • Resolución de dependencias +│ │ • Evaluación de condiciones +│ │ • Manejo de errores por ejecutor +│ │ • Eventos de ejecución +│ │ • Estadísticas de rendimiento +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ PipelineResult │ ← Resultado final de la ejecución +│ │ +│ status: Success │ +│ outputs: {...} │ +│ events: [...] │ +│ duration: 45.2s │ +└─────────────────┘ +``` + +### 5.2 Jerarquía de clases de ejecutores + +``` +Executor (Agent Framework) + │ + └── BaseExecutor (ABC) ─── contentflow/executors/base.py + │ + │ Propiedades comunes: + │ ├── id: str + │ ├── settings: Dict + │ ├── enabled: bool + │ ├── condition: str (evaluación condicional) + │ ├── fail_pipeline_on_error: bool + │ └── debug_mode: bool + │ + │ Métodos: + │ ├── get_setting(key, default) + │ ├── resolve_env_vars(value) + │ ├── _evaluate_condition(content) + │ └── process_input(input, ctx) → Content | List[Content] [ABSTRACTO] + │ + ├── InputExecutor (ABC) ─── contentflow/executors/input_executor.py + │ │ + │ │ Propiedades adicionales: + │ │ ├── polling_interval_seconds: int (300) + │ │ ├── max_results: int (1000) + │ │ └── batch_size: int (100) + │ │ + │ │ Métodos: + │ │ ├── crawl(checkpoint) → (List[Content], token) [ABSTRACTO] + │ │ └── crawl_all(checkpoint) → List[Content] + │ │ + │ └── Implementaciones: + │ └── AzureBlobInputDiscoveryExecutor + │ + └── Ejecutores concretos (35+): + ├── Extracción + ├── Procesamiento IA + ├── Transformación + ├── Salida + ├── Enrutamiento + └── Especializados +``` + +### 5.3 Catálogo completo de ejecutores + +#### Entrada y recuperación de contenido + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `azure_blob_input_discovery` | Azure Blob Input Discovery | Descubre archivos en contenedores Blob Storage | +| `azure_blob_content_retriever` | Azure Blob Content Retriever | Recupera el contenido de blobs descubiertos | +| `content_retriever` | Content Retriever | Recuperador genérico de contenido | + +#### Extracción de documentos + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `pdf_extractor` | PDF Extractor | Extrae texto de documentos PDF | +| `word_extractor` | Word Extractor | Extrae texto de documentos Word (.docx) | +| `excel_extractor` | Excel Extractor | Extrae datos de hojas de cálculo Excel | +| `powerpoint_extractor` | PowerPoint Extractor | Extrae texto de presentaciones PowerPoint | +| `csv_extractor` | CSV Extractor | Extrae datos de archivos CSV | +| `azure_document_intelligence_extractor` | Azure Document Intelligence | Extracción inteligente con OCR y modelos pre-entrenados | +| `azure_content_understanding_extractor` | Azure Content Understanding | Análisis avanzado de contenido con AI | + +#### Procesamiento con IA + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `azure_openai_agent` | Azure OpenAI Agent | Procesamiento con modelos GPT (análisis, generación) | +| `azure_openai_embeddings` | Azure OpenAI Embeddings | Genera vectores de embedding para búsqueda semántica | +| `text_summarizer` | Text Summarizer | Genera resúmenes de texto usando IA | +| `entity_extractor` | Entity Extractor | Extrae entidades nombradas (personas, organizaciones, lugares) | +| `sentiment_analyser` | Sentiment Analyser | Análisis de sentimiento (positivo, negativo, neutro) | +| `content_classifier` | Content Classifier | Clasifica contenido en categorías definidas | +| `pii_detector` | PII Detector | Detecta información personal identificable | +| `keyword_extractor` | Keyword Extractor | Extrae palabras clave y frases importantes | +| `language_detector` | Language Detector | Detecta el idioma del contenido | +| `content_translator` | Content Translator | Traduce contenido entre idiomas | + +#### Transformación de datos + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `recursive_text_chunker` | Recursive Text Chunker | Divide texto en fragmentos con solapamiento configurable | +| `table_row_splitter` | Table Row Splitter | Divide tablas en filas individuales para procesamiento | +| `field_mapper` | Field Mapper | Mapea y transforma campos entre el modelo de contenido | +| `field_selector` | Field Selector | Selecciona campos específicos del contenido | +| `fan_in_aggregator` | Fan-In Aggregator | Agrega resultados de procesamiento paralelo | + +#### Salida y almacenamiento + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `azure_blob_output` | Azure Blob Output | Escribe contenido procesado a Blob Storage | +| `ai_search_index_output` | AI Search Index Output | Indexa contenido en Azure AI Search | +| `gptrag_search_index_document_generator` | GPT RAG Index Generator | Genera documentos para índices RAG | + +#### Enrutamiento y orquestación + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `subpipeline` | Sub-Pipeline | Ejecuta un sub-pipeline anidado | +| `for_each_content` | For Each Content | Itera sobre una lista de contenidos | +| `pass_through` | Pass Through | Pasa contenido sin modificar (debug/testing) | + +#### Conjuntos de documentos + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `document_set_initializer` | Document Set Initializer | Inicializa un conjunto de documentos para procesamiento cruzado | +| `document_set_collector` | Document Set Collector | Recopila documentos en un conjunto | +| `cross_document_comparison` | Cross-Document Comparison | Compara contenido entre múltiples documentos | +| `cross_document_field_aggregator` | Cross-Document Field Aggregator | Agrega campos de múltiples documentos | + +#### Especializados + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `web_scraper` | Web Scraper | Extrae contenido de páginas web (Playwright) | +| `cosmos_db_lookup` | Cosmos DB Lookup | Consulta datos en Azure Cosmos DB | + +### 5.4 Modelo de contenido (Content) + +El modelo `Content` es la estructura de datos central que fluye por los pipelines: + +``` +Content +│ +├── Identificación +│ ├── id: str (UUID único) +│ └── source: str (origen: blob://container/path/file.pdf) +│ +├── Datos extraídos +│ ├── content: str (texto completo extraído) +│ ├── content_type: str (MIME type) +│ └── content_hash: str (hash para detección de cambios) +│ +├── Metadatos +│ ├── metadata: Dict[str, Any] +│ │ ├── filename, size, last_modified +│ │ ├── page_count, language +│ │ └── custom fields... +│ │ +│ └── tags: List[str] +│ +├── Fragmentos y embeddings +│ ├── chunks: List[Dict] (fragmentos de texto) +│ └── embeddings: List[float] (vectores numéricos) +│ +├── Resultados de análisis +│ ├── analysis: Dict[str, Any] +│ │ ├── entities, sentiment, keywords +│ │ ├── summary, classification +│ │ └── pii_detected, language +│ │ +│ └── executor_outputs: Dict[str, Any] +│ └── {executor_id: output_data} +│ +└── Registro de ejecución + └── log_entries: List[ExecutorLogEntry] + └── {executor_id, timestamp, status, duration_ms} +``` + +### 5.5 Conectores de Azure + +``` +contentflow/connectors/ +│ +├── Azure Blob Storage Connector +│ ├── Listar blobs en contenedores +│ ├── Descargar contenido de blobs +│ ├── Subir contenido a blobs +│ └── Gestionar metadatos de blobs +│ +├── Azure AI Search Connector +│ ├── Crear/actualizar índices +│ ├── Indexar documentos +│ └── Búsqueda híbrida (texto + vectores) +│ +├── Azure Document Intelligence Connector +│ ├── Analizar documentos (OCR) +│ ├── Modelos pre-entrenados (facturas, recibos, etc.) +│ └── Modelos personalizados +│ +├── Azure OpenAI Connector +│ ├── Completions (GPT-4.1, GPT-4.1-mini) +│ ├── Embeddings (text-embedding-3-small) +│ └── Chat completions +│ +└── Azure Cosmos DB Connector + ├── Consultas SQL NoSQL + ├── CRUD de documentos + └── Bulk operations +``` + +### 5.6 Diagrama del motor de pipelines + +``` +Definición YAML del Pipeline +┌──────────────────────────────────────────────────┐ +│ name: procesar-documentos │ +│ steps: │ +│ - executor: blob_input ─────────┐ │ +│ - executor: pdf_extractor ──────┐ │ │ +│ - executor: chunker ─────────┐ │ │ │ +│ - executor: embeddings ────┐ │ │ │ │ +│ - executor: blob_output ─┐ │ │ │ │ │ +└────────────────────────────┼─┼─┼──┼──┼────────────┘ + │ │ │ │ │ + PipelineFactory.parse() + │ + ▼ + ┌─ DAG de ejecución ────────────┐ + │ │ + │ [blob_input] │ + │ │ │ + │ ▼ │ + │ [pdf_extractor] │ + │ │ │ + │ ▼ │ + │ [chunker] │ + │ │ │ + │ ▼ │ + │ [embeddings] │ + │ │ │ + │ ▼ │ + │ [blob_output] │ + │ │ + └────────────────────────────────┘ + │ + PipelineExecutor.execute() + │ + ▼ + ┌─ Ejecución paso a paso ───────┐ + │ │ + │ Content ──▶ blob_input │ + │ Content ──▶ pdf_extractor │ + │ Content ──▶ chunker │ + │ Content ──▶ embeddings │ + │ Content ──▶ blob_output │ + │ │ + │ Cada paso: │ + │ 1. Evaluar condición │ + │ 2. Si enabled: ejecutar │ + │ 3. Capturar resultado/error │ + │ 4. Emitir evento │ + │ 5. Pasar al siguiente │ + │ │ + └────────────────────────────────┘ + │ + ▼ + PipelineResult + (status, outputs, + events, duration) +``` + +--- + +## 6. Infraestructura de Azure (infra) + +### 6.1 Recursos desplegados + +ContentFlow despliega los siguientes recursos de Azure: + +``` +Resource Group +│ +├── User-Assigned Managed Identity (id-${token}) +│ +├── Log Analytics Workspace (log-${token}) +│ └── Recolección de logs de todos los servicios +│ +├── Application Insights (appi-${token}) +│ └── Monitoreo, métricas y trazas distribuidas +│ +├── Cosmos DB Account (cosmos-${token}) +│ └── Database: contentflow +│ ├── executor_catalog +│ ├── pipelines +│ ├── vaults +│ ├── vault_executions +│ ├── vault_exec_locks +│ ├── vault_crawl_checkpoints +│ └── pipeline_executions +│ +├── Storage Account (st${token}) +│ ├── Blob Container: content +│ └── Queue: contentflow-execution-requests +│ +├── Container Registry (cr${token}) +│ └── Imágenes Docker de los 3 servicios +│ +├── App Configuration Store (appcs-${token}) +│ └── 25+ claves de configuración +│ +├── Container Apps Environment (cae-${token}) +│ ├── Container App: api-${token} (Puerto 8090) +│ ├── Container App: worker-${token} (Puerto 8099) +│ └── Container App: web-${token} +│ +└── AI Foundry (opcional) + ├── AI Services (S0) + └── Deployments: + ├── gpt-4.1-mini (GlobalStandard, Cap: 100) + └── gpt-4.1 (GlobalStandard, Cap: 100) +``` + +### 6.2 SKUs y niveles de servicio + +| Recurso | SKU / Nivel | Detalles | +|---------|------------|----------| +| **Cosmos DB** | Serverless | Pago por consumo, sin capacidad reservada | +| **Storage Account** | Standard_LRS | Locally Redundant Storage, Hot tier | +| **Container Registry** | Standard | Premium si se usan endpoints privados | +| **App Configuration** | Standard | Configuración centralizada | +| **Log Analytics** | PerGB2018 | Pago por GB ingerido | +| **Application Insights** | Workspace-based | Conectado a Log Analytics | +| **AI Foundry / AI Services** | S0 | Tier estándar de servicios cognitivos | +| **GPT-4.1** | GlobalStandard | Capacidad: 100 TPM | +| **GPT-4.1-mini** | GlobalStandard | Capacidad: 100 TPM | +| **Container Apps** | Consumption | Serverless, perfil de carga de trabajo por consumo | +| **Managed Identity** | User-Assigned | N/A (sin SKU, es un recurso de identidad) | + +### 6.3 Módulos Bicep + +| Módulo | Archivo | Azure Verified Module (AVM) | Versión AVM | +|--------|---------|----------------------------|-------------| +| Identidad administrada | `user-assigned-identity.bicep` | `avm/res/managed-identity/user-assigned-identity` | 0.4.1 | +| Log Analytics | `log-analytics-ws.bicep` | Built-in | — | +| Application Insights | `app-insights.bicep` | Built-in | — | +| Cosmos DB | `cosmos.bicep` | `avm/res/document-db/database-account` | 0.16.0 | +| Storage Account | `storage.bicep` | `avm/res/storage/storage-account` | 0.27.1 | +| Container Registry | `container-registry.bicep` | `avm/res/container-registry` | Built-in | +| Container Apps Environment | `container-apps-environment.bicep` | `avm/res/app/managed-environment` | 0.11.3 | +| Container App | `container-app.bicep` | `avm/res/app/container-app` | 0.19.0 | +| App Configuration | `app-config-store.bicep` | Built-in | — | +| AI Foundry | `ai-foundry.bicep` | `avm/ptn/ai-ml/ai-foundry` | 0.5.0 | +| Static Web App | `static-web-app.bicep` | Built-in (no activo) | — | + +### 6.4 Cosmos DB — Estructura de base de datos + +``` +Cosmos DB Account (cosmos-${token}) +│ +├── Tipo: NoSQL (SQL API) +├── Consistencia: Session +├── Replicación: Región única (automaticFailover: false) +├── Backup: Continuous7Days (hasta 30 días de retención) +├── Autenticación: Solo Managed Identity (contraseñas deshabilitadas) +│ +└── Database: "contentflow" + │ + ├── executor_catalog (/id) + │ └── Definiciones de ejecutores del catálogo + │ + ├── pipelines (/id) + │ └── Configuración y YAML de cada pipeline + │ + ├── vaults (/id) + │ └── Definiciones de bóvedas de documentos + │ + ├── vault_executions (/id) + │ └── Registro de ejecuciones por vault + │ + ├── vault_exec_locks (/id) + │ └── Bloqueos distribuidos para evitar ejecuciones duplicadas + │ + ├── vault_crawl_checkpoints (/id) + │ └── Puntos de control para rastreo incremental + │ + └── pipeline_executions (/id) + └── Historial detallado de ejecuciones de pipelines +``` + +### 6.5 Storage Account — Blobs y colas + +``` +Storage Account (st${token}) +│ +├── Tipo: StorageV2 +├── SKU: Standard_LRS +├── Acceso: Hot tier +├── Autenticación: Solo Managed Identity (sharedKeyAccess: false) +├── Retención de eliminados: 7 días (blobs y contenedores) +│ +├── Blob Containers: +│ └── "content" (acceso público: None) +│ ├── Documentos originales +│ └── Contenido procesado +│ +└── Queues: + └── "contentflow-execution-requests" + └── Mensajes de tareas (InputSource y ContentProcessing) +``` + +### 6.6 Container Apps — Configuración de servicios + +| Configuración | API | Worker | Web | +|---------------|-----|--------|-----| +| **Puerto** | 8090 | 8099 | HTTP estándar | +| **CPU** | 1 core | 1 core | 1 core | +| **Memoria** | 2 GiB | 2 GiB | 2 GiB | +| **Réplicas mínimas** | 1 | 1 | 1 | +| **Réplicas máximas** | 2 | 2 | 2 | +| **Ingress** | Externo | Externo | Externo | +| **Escalado** | HTTP (10 concurrentes) | HTTP (10 concurrentes) | HTTP (10 concurrentes) | +| **Health check** | `/health` (60s intervalo) | — | — | +| **CORS** | GET, POST, PUT, DELETE, OPTIONS, PATCH | GET, POST, PUT, DELETE, OPTIONS, PATCH | GET, POST, PUT, DELETE, OPTIONS, PATCH | +| **Perfil** | Consumption | Consumption | Consumption | + +### 6.7 AI Foundry — Modelos de IA + +``` +AI Foundry +│ +├── AI Services Account (S0) +│ └── Nombre del proyecto: "contentflow-project" +│ +└── Model Deployments: + │ + ├── gpt-4.1-mini + │ ├── Formato: OpenAI + │ ├── Versión: 2025-04-14 + │ ├── SKU: GlobalStandard + │ └── Capacidad: 100 (TPM) + │ + └── gpt-4.1 + ├── Formato: OpenAI + ├── Versión: 2025-04-14 + ├── SKU: GlobalStandard + └── Capacidad: 100 (TPM) +``` + +### 6.8 Diagrama de infraestructura Azure + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ RESOURCE GROUP │ +│ │ +│ ┌─ Identidad ─────────────┐ ┌─ Monitoreo ────────────────────┐ │ +│ │ │ │ │ │ +│ │ Managed Identity │ │ Log Analytics App Insights │ │ +│ │ (User-Assigned) │ │ (PerGB2018) (Workspace) │ │ +│ │ │ │ │ │ +│ │ RBAC Roles: │ └─────────────────────────────────┘ │ +│ │ • Blob Data Contributor │ │ +│ │ • Queue Data Contributor│ ┌─ Configuración ────────────────┐ │ +│ │ • Cosmos DB Data Contrib│ │ │ │ +│ └──────────────────────────┘ │ App Configuration (Standard) │ │ +│ │ 25+ claves de configuración │ │ +│ ┌─ Cómputo ───────────────────┐│ │ │ +│ │ ││ Claves: │ │ +│ │ Container Apps Environment ││ • COSMOS_DB_ENDPOINT │ │ +│ │ (Consumption workload) ││ • BLOB_STORAGE_ACCOUNT_NAME │ │ +│ │ ││ • STORAGE_WORKER_QUEUE_URL │ │ +│ │ ┌──────┐ ┌──────┐ ┌──────┐ ││ • ... │ │ +│ │ │ API │ │Worker│ │ Web │ │└─────────────────────────────────┘ │ +│ │ │ 8090 │ │ 8099 │ │ 80 │ │ │ +│ │ │ 1CPU │ │ 1CPU │ │ 1CPU │ │ ┌─ IA ────────────────────────┐ │ +│ │ │ 2GiB │ │ 2GiB │ │ 2GiB │ │ │ │ │ +│ │ └──────┘ └──────┘ └──────┘ │ │ AI Foundry (S0) │ │ +│ │ │ │ ├── gpt-4.1-mini (100 TPM)│ │ +│ │ Container Registry (Std) │ │ └── gpt-4.1 (100 TPM) │ │ +│ │ (Imágenes Docker) │ │ │ │ +│ └──────────────────────────────┘ └─────────────────────────────┘ │ +│ │ +│ ┌─ Datos ──────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ Cosmos DB (Serverless) Storage Account (Standard_LRS) │ │ +│ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ +│ │ │ DB: contentflow │ │ Blob: "content" │ │ │ +│ │ │ ├── executor_catalog│ │ Queue: "contentflow- │ │ │ +│ │ │ ├── pipelines │ │ execution-requests" │ │ │ +│ │ │ ├── vaults │ └────────────────────────────┘ │ │ +│ │ │ ├── vault_executions│ │ │ +│ │ │ ├── vault_exec_locks│ │ │ +│ │ │ ├── vault_crawl_ │ │ │ +│ │ │ │ checkpoints │ │ │ +│ │ │ └── pipeline_ │ │ │ +│ │ │ executions │ │ │ +│ │ └─────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 7. Interconexión entre componentes + +### 7.1 Flujo de datos completo + +``` +┌─────────┐ HTTP ┌─────────┐ Write ┌──────────┐ Poll ┌──────────┐ +│ WEB │────────▶│ API │────────▶│ QUEUE │────────▶│ WORKER │ +│ (React) │ REST │(FastAPI)│ Queue │ (Azure │ Recv │ (Python) │ +└─────────┘ └────┬────┘ │ Storage) │ └────┬─────┘ + │ └──────────┘ │ + │ │ + ┌────▼────┐ ┌────▼─────┐ + │COSMOS DB│◀─────────────────────────────│ PIPELINE │ + │(Metadat)│ Lee pipelines, actualiza │ ENGINE │ + └─────────┘ estados de ejecución │ (Lib) │ + │ └────┬─────┘ + ┌────▼────┐ │ + │ BLOB │◀───────────────────────────── │ + │STORAGE │ Lee documentos, guarda │ + │(Content)│ resultados procesados │ + └─────────┘ │ + ┌────▼─────┐ + │AZURE AI │ + │SERVICES │ + │(GPT, Doc │ + │ Intell.) │ + └──────────┘ +``` + +### 7.2 Flujo de mensajes por cola + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ FLUJO DE MENSAJES POR COLA │ +│ │ +│ ① API recibe solicitud de ejecución │ +│ POST /api/pipelines/{id}/execute │ +│ │ │ +│ ▼ │ +│ ② API crea registro de ejecución en Cosmos DB │ +│ PipelineExecution { status: "pending" } │ +│ │ │ +│ ▼ │ +│ ③ API envía InputSourceTask a la cola │ +│ ┌───────────────────────────────────────────┐ │ +│ │ Queue: contentflow-execution-requests │ │ +│ │ Message: { task_type: "INPUT_SOURCE", │ │ +│ │ payload: { pipeline_id, ... } } │ │ +│ └────────────────────┬──────────────────────┘ │ +│ │ │ +│ ④ Source Worker consume la tarea │ +│ │ │ +│ ▼ │ +│ ⑤ Source Worker ejecuta ejecutores de input │ +│ → Descubre N documentos │ +│ │ │ +│ ▼ │ +│ ⑥ Source Worker crea N tareas ContentProcessingTask │ +│ ┌───────────────────────────────────────────┐ │ +│ │ Queue: contentflow-execution-requests │ │ +│ │ Message × N: │ │ +│ │ { task_type: "CONTENT_PROCESSING", │ │ +│ │ payload: { pipeline_id, content, ... } } │ │ +│ └────────────────────┬──────────────────────┘ │ +│ │ │ +│ ⑦ Processing Workers consumen tareas (en paralelo) │ +│ │ │ +│ ▼ │ +│ ⑧ Cada worker ejecuta el pipeline completo sobre un documento │ +│ (excluyendo ejecutores de input) │ +│ │ │ +│ ▼ │ +│ ⑨ Resultados almacenados en Blob Storage / Cosmos DB │ +│ PipelineExecution { status: "completed" } │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 7.3 Flujo de autenticación + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ CADENA DE AUTENTICACIÓN │ +│ │ +│ User-Assigned Managed Identity │ +│ (id-${resourceToken}) │ +│ │ │ +│ ├── DefaultAzureCredential │ +│ │ (Cadena de credenciales automática) │ +│ │ │ +│ ├──▶ Cosmos DB ──── rol: "Cosmos DB SQL Data Contributor" │ +│ │ (Lectura/escritura de documentos) │ +│ │ │ +│ ├──▶ Blob Storage ─ rol: "Storage Blob Data Contributor" │ +│ │ (Lectura/escritura de blobs) │ +│ │ │ +│ ├──▶ Storage Queue ─ rol: "Storage Queue Data Contributor"│ +│ │ (Envío/recepción de mensajes) │ +│ │ │ +│ └──▶ App Config ──── rol: "App Configuration Data Reader" │ +│ (Lectura de configuración) │ +│ │ +│ Nota: Contraseñas y claves compartidas están DESHABILITADAS │ +│ (disableLocalAuthentication: true, allowSharedKeyAccess: false) │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### 7.4 Matriz de comunicación entre servicios + +| Origen | Destino | Protocolo | Propósito | +|--------|---------|-----------|-----------| +| **Web** → API | HTTP/REST | CRUD de pipelines, vaults, catálogo | +| **API** → Cosmos DB | SDK Azure (HTTPS) | Almacenar/leer metadatos | +| **API** → Blob Storage | SDK Azure (HTTPS) | Gestionar documentos | +| **API** → Storage Queue | SDK Azure (HTTPS) | Encolar tareas | +| **API** → App Configuration | SDK Azure (HTTPS) | Leer configuración | +| **Worker** → Storage Queue | SDK Azure (HTTPS) | Consumir tareas | +| **Worker** → Cosmos DB | SDK Azure (HTTPS) | Leer pipelines, actualizar estados | +| **Worker** → Blob Storage | SDK Azure (HTTPS) | Leer/escribir contenido | +| **Worker** → Azure AI Services | SDK Azure (HTTPS) | Procesamiento IA | +| **Worker** → App Configuration | SDK Azure (HTTPS) | Leer configuración | +| **Todos** → App Insights | SDK Azure (HTTPS) | Telemetría y trazas | + +### 7.5 Diagrama de secuencia: Ejecución de pipeline + +``` + Usuario Web (React) API (FastAPI) Cosmos DB Storage Queue Source Worker Processing Worker Azure AI + │ │ │ │ │ │ │ │ + │ ① Crear │ │ │ │ │ │ │ + │ pipeline │ │ │ │ │ │ │ + │───────────────▶│ │ │ │ │ │ │ + │ │ POST /pipeline │ │ │ │ │ │ + │ │───────────────▶│ │ │ │ │ │ + │ │ │──── save ────▶│ │ │ │ │ + │ │ │◀──── ok ──────│ │ │ │ │ + │ │◀───── 201 ─────│ │ │ │ │ │ + │◀───────────────│ │ │ │ │ │ │ + │ │ │ │ │ │ │ │ + │ ② Ejecutar │ │ │ │ │ │ │ + │───────────────▶│ │ │ │ │ │ │ + │ │ POST /execute │ │ │ │ │ │ + │ │───────────────▶│ │ │ │ │ │ + │ │ │── save exec ─▶│ │ │ │ │ + │ │ │── queue task ─┼─────────────▶│ │ │ │ + │ │◀─── 202 ──────│ │ │ │ │ │ + │◀───────────────│ │ │ │ │ │ │ + │ │ │ │ │ │ │ │ + │ │ │ │ │ ③ poll │ │ │ + │ │ │ │ │◀─────────────│ │ │ + │ │ │ │ │── InputTask──▶ │ │ + │ │ │ │◀─── read pipeline ──────────│ │ │ + │ │ │ │──── pipeline data ─────────▶│ │ │ + │ │ │ │ │ │ │ │ + │ │ │ │ │ ④ descubrir │ │ │ + │ │ │ │ │ N documentos│ │ │ + │ │ │ │ │◀── N tasks ──│ │ │ + │ │ │ │ │ │ │ │ + │ │ │ │ │ ⑤ poll │ │ │ + │ │ │ │ │◀─────────────┼───────────────│ │ + │ │ │ │ │─ ContentTask─┼──────────────▶│ │ + │ │ │ │◀────────── read pipeline ───┼───────────────│ │ + │ │ │ │ │ │ │ + │ │ │ │ │ │ ⑥ ejecutar │ │ + │ │ │ │ │ │ pipeline │ │ + │ │ │ │ │ │──────────────▶│──── AI ─────▶│ + │ │ │ │ │ │ │◀─── result ──│ + │ │ │ │◀──────────── update status ─┼───────────────│ │ + │ │ │ │ │ │ │ │ + │ ⑦ Consultar │ │ │ │ │ │ │ + │───────────────▶│ GET /executions│ │ │ │ │ │ + │ │───────────────▶│── query ─────▶│ │ │ │ │ + │ │ │◀── results ───│ │ │ │ │ + │ │◀──── 200 ──────│ │ │ │ │ │ + │◀───────────────│ │ │ │ │ │ │ +``` + +--- + +## 8. Modos de despliegue + +### 8.1 Modo básico + +``` +┌─────────────────────────────────────────────────────────────┐ +│ MODO BÁSICO │ +│ │ +│ Ideal para: Desarrollo, pruebas, demos │ +│ │ +│ Características: │ +│ ✓ Endpoints públicos (accesibles desde internet) │ +│ ✓ Todos los recursos creados nuevos │ +│ ✓ Red virtual creada automáticamente │ +│ ✓ Log Analytics + App Insights propios │ +│ ✗ Sin endpoints privados │ +│ ✗ Sin integración con redes existentes │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ INTERNET │ │ +│ │ │ │ │ +│ │ ┌──────────────────┼───────────────────┐ │ │ +│ │ │ Container Apps Environment │ │ │ +│ │ │ │ │ │ │ +│ │ │ ┌──────┐ ┌───┴───┐ ┌──────┐ │ │ │ +│ │ │ │ Web │ │ API │ │Worker│ │ │ │ +│ │ │ │(pub) │ │(pub) │ │(pub) │ │ │ │ +│ │ │ └──────┘ └───────┘ └──────┘ │ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ┌──────────────────┼───────────────────┐ │ │ +│ │ │ Recursos con acceso público │ │ │ +│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ +│ │ │ │CosmosDB│ │Storage │ │AppConf │ │ │ │ +│ │ │ │(public)│ │(public)│ │(public) │ │ │ │ +│ │ │ └────────┘ └────────┘ └────────┘ │ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 8.2 Modo AILZ (AI Landing Zone) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ MODO AILZ (AI Landing Zone) │ +│ │ +│ Ideal para: Producción empresarial │ +│ │ +│ Características: │ +│ ✓ Endpoints privados para todos los datos │ +│ ✓ Integración con VNet existente │ +│ ✓ Zonas DNS privadas │ +│ ✓ Shared Log Analytics + App Insights (del Landing Zone) │ +│ ✓ Container Registry Premium (endpoints privados) │ +│ ✓ Cumplimiento de seguridad empresarial │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ AI Landing Zone VNet (Existente) │ │ +│ │ │ │ +│ │ ┌─ aca-env-subnet ────────────────────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ Container Apps Environment (internal VNet) │ │ │ +│ │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ +│ │ │ │ Web │ │ API │ │Worker│ │ │ │ +│ │ │ └──────┘ └──────┘ └──────┘ │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─ pe-subnet (Private Endpoints) ─────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ ┌────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ │ │ +│ │ │ │CosmosDB│ │Blob PE │ │Queue PE │ │ACR PE │ │ │ │ +│ │ │ │ PE │ │ │ │ │ │ │ │ │ │ +│ │ │ └────────┘ └────────┘ └──────────┘ └────────┘ │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ Zonas DNS Privadas: │ │ +│ │ • *.documents.azure.com (Cosmos DB) │ │ +│ │ • *.blob.core.windows.net (Blob Storage) │ │ +│ │ • *.queue.core.windows.net (Storage Queue) │ │ +│ │ • *.azurecr.io (Container Registry) │ │ +│ │ • *.azconfig.io (App Configuration) │ │ +│ │ • *.cognitiveservices.azure.com (AI Services) │ │ +│ │ • Container Apps custom domain │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 8.3 Proceso de despliegue con azd + +``` +$ azd up +│ +├── ① pre-provision.sh +│ ├── Validar Azure CLI instalado +│ ├── Validar azd instalado +│ └── Validar Docker instalado +│ +├── ② Provisionar infraestructura (Bicep) +│ ├── Crear Managed Identity +│ ├── Crear Log Analytics + App Insights (básico) +│ ├── Crear Cosmos DB + containers +│ ├── Crear Storage Account + blob + queue +│ ├── Crear Container Registry +│ ├── Crear App Configuration + claves +│ ├── Crear Container Apps Environment +│ ├── Crear Container Apps (API, Worker, Web) +│ ├── Crear AI Foundry + deployments (opcional) +│ └── Asignar roles RBAC +│ +├── ③ Build y Push de imágenes Docker +│ ├── docker build contentflow-api/ +│ ├── docker build contentflow-worker/ +│ ├── docker build contentflow-web/ +│ └── docker push → Container Registry +│ +├── ④ Desplegar servicios en Container Apps +│ ├── Actualizar imagen del API +│ ├── Actualizar imagen del Worker +│ └── Actualizar imagen del Web +│ +├── ⑤ post-deploy-api.sh +│ └── Configuración post-despliegue del API +│ +└── ⑥ post-deploy.sh + └── Tareas finales interactivas + +Resultado: URLs de los servicios desplegados + → API: https://api-xxxx.azurecontainerapps.io + → Worker: https://worker-xxxx.azurecontainerapps.io + → Web: https://web-xxxx.azurecontainerapps.io +``` + +### 8.4 Diagrama comparativo de modos + +``` + MODO BÁSICO MODO AILZ + ┌─────────────────────┐ ┌─────────────────────────┐ + │ │ │ │ + │ ☁️ Internet │ │ ☁️ Internet (limitado) │ + │ │ │ │ │ │ + │ ┌────▼────┐ │ │ ┌────▼────┐ │ + │ │Container│ │ │ │Container│ │ + │ │Apps │ │ │ │Apps │ │ + │ │(público)│ │ │ │(VNet │ │ + │ └────┬────┘ │ │ │internal)│ │ + │ │ │ │ └────┬────┘ │ + │ ┌────▼───┐ │ │ ┌────▼────┐ │ + │ │Recursos│ │ │ │ Private │ │ + │ │público │ │ │ │Endpoints│ │ + │ │acceso │ │ │ │(VNet) │ │ + │ └────────┘ │ │ └─────────┘ │ + │ │ │ │ + │ ✅ Rápido │ │ ✅ Seguro │ + │ ✅ Simple │ │ ✅ Empresarial │ + │ ❌ No para prod. │ │ ❌ Requiere VNet/LZ │ + └─────────────────────┘ └─────────────────────────┘ +``` + +--- + +## 9. Seguridad y autenticación + +### 9.1 Modelo de identidad + +ContentFlow utiliza **Azure Managed Identity** (identidad administrada asignada por el usuario) para toda la autenticación entre servicios. No se utilizan contraseñas, claves de acceso ni cadenas de conexión. + +``` +┌──────────────────────────────────────────────────────┐ +│ USER-ASSIGNED MANAGED IDENTITY │ +│ (id-${resourceToken}) │ +│ │ +│ Utilizada por: │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ API │ │ Worker │ │ Web │ │ +│ │ Service │ │ Service │ │ (build) │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ Autenticación en código: │ +│ DefaultAzureCredential() → cadena automática: │ +│ 1. Environment variables │ +│ 2. Managed Identity │ +│ 3. Azure CLI (desarrollo local) │ +│ 4. Azure PowerShell │ +└──────────────────────────────────────────────────────┘ +``` + +### 9.2 Roles RBAC asignados + +| Recurso | Rol RBAC | Asignado a | Permiso | +|---------|----------|-----------|---------| +| **Cosmos DB** | Cosmos DB SQL Data Contributor | Managed Identity | Leer/escribir datos | +| **Cosmos DB** | Cosmos DB SQL Data Contributor | Deployer (principal) | Leer/escribir datos | +| **Blob Storage** | Storage Blob Data Contributor | Managed Identity | Leer/escribir blobs | +| **Blob Storage** | Storage Blob Data Contributor | Deployer (principal) | Leer/escribir blobs | +| **Storage Queue** | Storage Queue Data Contributor | Managed Identity | Enviar/recibir mensajes | +| **Storage Queue** | Storage Queue Data Contributor | Deployer (principal) | Enviar/recibir mensajes | +| **App Configuration** | App Configuration Data Reader | Managed Identity | Leer configuración | +| **Container Registry** | AcrPull | Managed Identity | Descargar imágenes | + +### 9.3 Diagrama de seguridad + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ MODELO DE SEGURIDAD │ +│ │ +│ ┌─ Autenticación ─────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ Managed Identity (sin contraseñas) │ │ +│ │ ✅ DefaultAzureCredential (cadena automática) │ │ +│ │ ✅ disableLocalAuthentication: true (Cosmos DB) │ │ +│ │ ✅ allowSharedKeyAccess: false (Storage) │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Autorización ──────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ RBAC (Role-Based Access Control) para cada recurso │ │ +│ │ ✅ Principio de mínimo privilegio │ │ +│ │ ✅ Roles específicos por tipo de operación │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Red (modo AILZ) ──────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ Private Endpoints para datos │ │ +│ │ ✅ Subnets dedicadas │ │ +│ │ ✅ DNS privado │ │ +│ │ ✅ Network rules: Deny por defecto │ │ +│ │ ✅ Bypass: AzureServices │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Datos ─────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ Encriptación en tránsito (HTTPS/TLS) │ │ +│ │ ✅ Encriptación en reposo (Azure managed keys) │ │ +│ │ ✅ Backup continuo (Cosmos DB: 7-30 días) │ │ +│ │ ✅ Soft delete (Blob Storage: 7 días retención) │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 10. Configuración centralizada + +### 10.1 Azure App Configuration + +Todas las configuraciones de runtime se almacenan en **Azure App Configuration** y se leen tanto por el API como por el Worker: + +``` +App Configuration Store (appcs-${token}) +│ +├── Cosmos DB +│ ├── COSMOS_DB_ENDPOINT: https://cosmos-xxx.documents.azure.com +│ ├── COSMOS_DB_NAME: contentflow +│ ├── COSMOS_DB_CONTAINER_PIPELINES: pipelines +│ ├── COSMOS_DB_CONTAINER_VAULTS: vaults +│ ├── COSMOS_DB_CONTAINER_VAULT_EXECUTIONS: vault_executions +│ ├── COSMOS_DB_CONTAINER_VAULT_EXECUTION_LOCKS: vault_exec_locks +│ ├── COSMOS_DB_CONTAINER_CRAWL_CHECKPOINTS: vault_crawl_checkpoints +│ └── COSMOS_DB_CONTAINER_PIPELINE_EXECUTIONS: pipeline_executions +│ +├── Storage +│ ├── BLOB_STORAGE_ACCOUNT_NAME: stxxxx +│ ├── BLOB_STORAGE_CONTAINER_NAME: content +│ ├── STORAGE_ACCOUNT_WORKER_QUEUE_URL: https://stxxxx.queue.core.windows.net +│ └── STORAGE_WORKER_QUEUE_NAME: contentflow-execution-requests +│ +├── Worker +│ ├── WORKER_ENGINE_API_ENDPOINT: https://worker-xxx.azurecontainerapps.io +│ ├── NUM_PROCESSING_WORKERS: 0 +│ └── NUM_SOURCE_WORKERS: 1 +│ +└── Application + ├── API_SERVER_PORT: 8090 + ├── LOG_LEVEL: DEBUG + └── DEBUG: true +``` + +### 10.2 Variables de entorno por servicio + +| Variable | API | Worker | Descripción | +|----------|:---:|:------:|-------------| +| `COSMOS_DB_ENDPOINT` | ✅ | ✅ | Endpoint de Cosmos DB | +| `COSMOS_DB_NAME` | ✅ | ✅ | Nombre de la base de datos | +| `BLOB_STORAGE_ACCOUNT_NAME` | ✅ | ✅ | Nombre de la cuenta de storage | +| `BLOB_STORAGE_CONTAINER_NAME` | ✅ | ✅ | Nombre del contenedor blob | +| `STORAGE_ACCOUNT_WORKER_QUEUE_URL` | ✅ | ✅ | URL de la cola | +| `STORAGE_WORKER_QUEUE_NAME` | ✅ | ✅ | Nombre de la cola | +| `API_SERVER_PORT` | ✅ | — | Puerto del API (8090) | +| `API_PORT` | — | ✅ | Puerto de health del worker (8099) | +| `NUM_PROCESSING_WORKERS` | — | ✅ | Workers de procesamiento | +| `NUM_SOURCE_WORKERS` | — | ✅ | Workers de entrada | +| `QUEUE_POLL_INTERVAL_SECONDS` | — | ✅ | Intervalo de poll (5s) | +| `DEFAULT_POLLING_INTERVAL_SECONDS` | — | ✅ | Intervalo de descubrimiento (300s) | +| `MAX_TASK_RETRIES` | — | ✅ | Reintentos por tarea (3) | +| `TASK_TIMEOUT_SECONDS` | — | ✅ | Timeout por tarea (600s) | +| `LOG_LEVEL` | ✅ | ✅ | Nivel de logging (DEBUG) | +| `DEBUG` | ✅ | ✅ | Modo debug | + +--- + +## 11. Monitoreo y observabilidad + +``` +┌─────────────────────────────────────────────────────────────┐ +│ STACK DE OBSERVABILIDAD │ +│ │ +│ ┌─ Application Insights ─────────────────────────────────┐ │ +│ │ │ │ +│ │ • Trazas distribuidas entre servicios │ │ +│ │ • Métricas de rendimiento (latencia, errores) │ │ +│ │ • Logs de aplicación (Python + React) │ │ +│ │ • Mapa de dependencias entre componentes │ │ +│ │ • Alertas configurables │ │ +│ │ │ │ +│ └─────────────────────┬───────────────────────────────────┘ │ +│ │ │ +│ ┌─ Log Analytics ─────▼──────────────────────────────────┐ │ +│ │ │ │ +│ │ • Logs de Container Apps (stdout/stderr) │ │ +│ │ • Logs de sistema de Container Apps Environment │ │ +│ │ • Métricas de infraestructura │ │ +│ │ • Consultas KQL personalizables │ │ +│ │ • Retención configurable │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ Fuentes de datos: │ +│ ├── API Service ──────▶ Logs HTTP, latencia, errores │ +│ ├── Worker Service ───▶ Logs de procesamiento, tareas │ +│ ├── Web Service ──────▶ Logs de build, errores client-side │ +│ └── Container Apps ───▶ Escalado, arranques, health checks │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 12. Resumen de dependencias entre componentes + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ MAPA DE DEPENDENCIAS DE CONTENTFLOW │ +│ │ +│ │ +│ contentflow-web ──────────────▶ contentflow-api │ +│ (Frontend) HTTP/REST (Backend) │ +│ │ │ +│ ├──── azure-cosmos (SDK) │ +│ ├──── azure-storage-blob (SDK) │ +│ ├──── azure-storage-queue (SDK) │ +│ ├──── azure-appconfiguration (SDK) │ +│ └──── contentflow-lib (local) │ +│ │ │ +│ contentflow-worker ─────────────────────────────┤ │ +│ (Processing) │ │ +│ │ │ │ +│ ├──── azure-cosmos (SDK) │ │ +│ ├──── azure-storage-queue (SDK) contentflow-lib │ +│ ├──── azure-storage-blob (SDK) (Core Engine) │ +│ └──── contentflow-lib (local) ──┐ │ │ +│ │ ├── Pipeline Engine │ +│ └──────────├── 35+ Ejecutores │ +│ ├── Content Model │ +│ ├── Conectores Azure │ +│ │ ├── Blob Storage │ +│ │ ├── AI Search │ +│ │ ├── Document Intel. │ +│ │ ├── OpenAI │ +│ │ └── Cosmos DB │ +│ └── Utilidades │ +│ │ +│ infra/ (Bicep) │ +│ │ │ +│ └──── Aprovisiona TODOS los recursos de Azure que los │ +│ servicios anteriores consumen │ +│ │ +│ azure.yaml │ +│ │ │ +│ └──── Orquesta el despliegue de infra/ + servicios │ +│ via Azure Developer CLI (azd) │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +> **Este documento refleja el estado de la arquitectura a la fecha de generación. Consulte el código fuente para la información más actualizada.** diff --git a/Analysis/03-use_cases_examples.md b/Analysis/03-use_cases_examples.md new file mode 100644 index 0000000..d2e641f --- /dev/null +++ b/Analysis/03-use_cases_examples.md @@ -0,0 +1,666 @@ +# ContentFlow — Casos de Uso y Ejemplos + +> **Catálogo de escenarios implementables con el acelerador ContentFlow**, organizados por industria y función, con los ejecutores recomendados para cada caso. + +--- + +## Índice + +- [1. Procesamiento de facturas y recibos](#1-procesamiento-de-facturas-y-recibos) +- [2. Indexación inteligente de contenido](#2-indexación-inteligente-de-contenido) +- [3. Análisis de contratos](#3-análisis-de-contratos) +- [4. Grafos de conocimiento evolutivos](#4-grafos-de-conocimiento-evolutivos) +- [5. Procesamiento de formularios](#5-procesamiento-de-formularios) +- [6. Cumplimiento normativo y gestión de riesgos](#6-cumplimiento-normativo-y-gestión-de-riesgos) +- [7. Procesamiento de contenido web](#7-procesamiento-de-contenido-web) +- [8. Análisis de imagen y contenido visual](#8-análisis-de-imagen-y-contenido-visual) +- [9. Procesamiento de adjuntos de correo electrónico](#9-procesamiento-de-adjuntos-de-correo-electrónico) +- [10. Ingestión de contenido para GPT-RAG](#10-ingestión-de-contenido-para-gpt-rag) +- [11. Resumen de artículos y reportes](#11-resumen-de-artículos-y-reportes) +- [12. Traducción de contenido multilingüe](#12-traducción-de-contenido-multilingüe) +- [13. Resumen de ejecutores por caso de uso](#13-resumen-de-ejecutores-por-caso-de-uso) +- [14. Pipelines de ejemplo incluidos en el repositorio](#14-pipelines-de-ejemplo-incluidos-en-el-repositorio) + +--- + +## 1. Procesamiento de facturas y recibos + +Automatiza la extracción de datos estructurados de facturas, recibos y órdenes de compra usando Azure Content Understanding. + +**Capacidades**: +- Extraer datos estructurados de facturas usando Azure Content Understanding +- Clasificar documentos por tipo (factura, recibo, orden de compra) +- Validar campos extraídos y señalar anomalías +- Exportar a sistemas ERP o bases de datos + +**Pipeline sugerido**: + +```yaml +name: procesamiento-facturas +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: facturas-entrantes + file_extensions: [".pdf", ".png", ".jpg"] + + - executor: azure_blob_content_retriever + + - executor: azure_content_understanding_extractor + settings: + model_id: prebuilt-invoice + + - executor: content_classifier + settings: + categories: ["factura", "recibo", "orden_de_compra"] + + - executor: field_mapper + settings: + mappings: + vendor_name: "proveedor" + total_amount: "monto_total" + invoice_date: "fecha_factura" + + - executor: azure_blob_output + settings: + blob_container_name: facturas-procesadas +``` + +**Ejecutores clave**: `azure_content_understanding_extractor`, `content_classifier`, `field_mapper`, `azure_blob_output` + +--- + +## 2. Indexación inteligente de contenido + +Procesa documentos, imágenes, audio y video para generar embeddings vectoriales e indexar en Azure AI Search, habilitando búsqueda semántica empresarial. + +**Capacidades**: +- Procesar documentos en múltiples formatos (PDF, Word, Excel, PowerPoint) +- Generar vectores de embedding para búsqueda semántica +- Indexar contenido en Azure AI Search +- Habilitar búsqueda semántica en todo el contenido empresarial + +**Pipeline sugerido**: + +```yaml +name: indexacion-inteligente +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documentos-empresa + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + settings: + model_id: prebuilt-layout + + - executor: recursive_text_chunker + settings: + chunk_size: 1000 + chunk_overlap: 200 + + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + - executor: ai_search_index_output + settings: + index_name: contenido-empresarial +``` + +**Ejecutores clave**: `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `ai_search_index_output` + +--- + +## 3. Análisis de contratos + +Extrae cláusulas clave, identifica partes involucradas, fechas, montos, y genera resúmenes automáticos para revisión legal. + +**Capacidades**: +- Extraer cláusulas clave y obligaciones contractuales +- Identificar partes, fechas y valores monetarios +- Señalar términos riesgosos usando análisis de IA +- Generar resúmenes para revisión legal + +**Pipeline sugerido**: + +```yaml +name: analisis-contratos +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: contratos + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: entity_extractor + settings: + entity_types: ["persona", "organizacion", "fecha", "monto"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1 + prompt: | + Analiza este contrato y extrae: + 1. Cláusulas clave y obligaciones + 2. Términos riesgosos o inusuales + 3. Fechas límite y vencimientos + 4. Valores monetarios y condiciones de pago + + - executor: text_summarizer + settings: + max_length: 500 + style: "resumen ejecutivo legal" + + - executor: azure_blob_output + settings: + blob_container_name: contratos-analizados +``` + +**Ejecutores clave**: `pdf_extractor`, `entity_extractor`, `azure_openai_agent`, `text_summarizer` + +--- + +## 4. Grafos de conocimiento evolutivos + +Construye grafos de conocimiento que mapean entidades, relaciones y estructuras organizativas a partir de documentos corporativos. + +**Capacidades**: +- Extraer entidades (personas, organizaciones, productos, ubicaciones) +- Identificar relaciones (trabaja_en, gestiona, ubicado_en) +- Mapear estructuras organizativas +- Descubrir conexiones ocultas entre documentos + +**Pipeline sugerido**: + +```yaml +name: grafo-conocimiento +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documentos-corporativos + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + + - executor: recursive_text_chunker + settings: + chunk_size: 2000 + + - executor: entity_extractor + settings: + entity_types: ["persona", "organizacion", "producto", "ubicacion"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1 + prompt: | + A partir de las entidades extraídas, identifica relaciones como: + - trabaja_en, gestiona, reporta_a + - ubicado_en, opera_en + - provee, compra_a, compite_con + Devuelve las relaciones en formato estructurado. + + - executor: azure_blob_output + settings: + blob_container_name: grafos-conocimiento +``` + +**Ejecutores clave**: `entity_extractor`, `azure_openai_agent`, `document_set_initializer`, `cross_document_comparison` + +--- + +## 5. Procesamiento de formularios + +Automatiza la captura y validación de datos de formularios, solicitudes, reclamaciones y aplicaciones. + +**Capacidades**: +- Procesar solicitudes, reclamaciones y formularios +- Extraer campos estructurados con ejecutores de IA +- Validar completitud y precisión de los datos +- Enrutar al sistema de línea de negocio (LoB) apropiado + +**Pipeline sugerido**: + +```yaml +name: procesamiento-formularios +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: formularios-entrantes + + - executor: azure_blob_content_retriever + + - executor: azure_content_understanding_extractor + settings: + model_id: prebuilt-document + + - executor: field_selector + settings: + fields: ["nombre", "fecha", "tipo_solicitud", "monto", "descripcion"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1-mini + prompt: | + Valida los campos extraídos: + - ¿Están todos los campos obligatorios presentes? + - ¿Los formatos son correctos (fechas, montos)? + - Señala campos faltantes o inconsistentes. + + - executor: content_classifier + settings: + categories: ["solicitud_credito", "reclamacion_seguro", "solicitud_empleo"] + + - executor: azure_blob_output + settings: + blob_container_name: formularios-procesados +``` + +**Ejecutores clave**: `azure_content_understanding_extractor`, `field_selector`, `content_classifier`, `azure_openai_agent` + +--- + +## 6. Cumplimiento normativo y gestión de riesgos + +Detecta información personal identificable (PII), clasifica documentos por sensibilidad y señala problemas de cumplimiento. + +**Capacidades**: +- Detectar PII (Información Personalmente Identificable) +- Clasificar documentos por nivel de sensibilidad +- Señalar problemas de cumplimiento normativo +- Extraer entidades clave y temas sensibles + +**Pipeline sugerido**: + +```yaml +name: cumplimiento-riesgos +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documentos-revision + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: pii_detector + settings: + pii_types: ["nombre", "email", "telefono", "numero_seguro_social", + "tarjeta_credito", "direccion", "fecha_nacimiento"] + + - executor: content_classifier + settings: + categories: ["publico", "interno", "confidencial", "restringido"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1 + prompt: | + Analiza el documento para cumplimiento normativo: + - ¿Contiene información regulada (GDPR, HIPAA, PCI-DSS)? + - ¿Se manejan datos sensibles correctamente? + - Identifica riesgos potenciales de cumplimiento. + Genera un reporte con nivel de riesgo: bajo, medio, alto, crítico. + + - executor: azure_blob_output + settings: + blob_container_name: reportes-cumplimiento +``` + +**Ejecutores clave**: `pii_detector`, `content_classifier`, `entity_extractor`, `azure_openai_agent` + +--- + +## 7. Procesamiento de contenido web + +Extrae, traduce y corrige contenido de sitios web para alimentar soluciones de chat basadas en RAG o sitios multilingües. + +**Capacidades**: +- Extraer contenido de sitios web para soluciones de chat basadas en RAG +- Traducir contenido web para experiencias multilingües +- Identificar y corregir errores ortográficos y de contenido + +**Pipeline sugerido**: + +```yaml +name: procesamiento-web +steps: + - executor: web_scraper + settings: + urls: + - "https://docs.empresa.com" + - "https://soporte.empresa.com" + max_depth: 3 + selectors: ["article", "main", ".content"] + + - executor: recursive_text_chunker + settings: + chunk_size: 1500 + + - executor: language_detector + + - executor: content_translator + settings: + target_language: "es" + + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + - executor: ai_search_index_output + settings: + index_name: contenido-web-rag +``` + +**Ejecutores clave**: `web_scraper`, `language_detector`, `content_translator`, `azure_openai_embeddings` + +--- + +## 8. Análisis de imagen y contenido visual + +Digitaliza notas clínicas manuscritas, extrae texto de reportes visuales y analiza imágenes de daños para procesamiento automatizado de reclamaciones. + +**Capacidades**: + +| Industria | Aplicación | +|-----------|-----------| +| **Salud** | Digitalizar notas clínicas manuscritas, extraer texto de reportes, generar descripciones de imágenes médicas para registros electrónicos de salud y cumplimiento de accesibilidad | +| **Seguros** | Procesar formularios manuscritos de reclamaciones, extraer datos de fotos de accidentes, analizar imágenes de evaluación de daños para procesamiento automatizado | + +**Pipeline sugerido**: + +```yaml +name: analisis-visual-seguros +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: reclamaciones-fotos + file_extensions: [".jpg", ".png", ".pdf"] + + - executor: azure_blob_content_retriever + + - executor: azure_content_understanding_extractor + settings: + model_id: prebuilt-document + + - executor: azure_openai_agent + settings: + model: gpt-4.1 + prompt: | + Analiza esta imagen/documento de reclamación de seguro: + - Identifica el tipo de daño visible + - Estima la severidad (leve, moderado, severo) + - Extrae información del formulario de reclamación + - Señala cualquier inconsistencia + + - executor: content_classifier + settings: + categories: ["vehicular", "propiedad", "salud", "vida"] + + - executor: azure_blob_output + settings: + blob_container_name: reclamaciones-procesadas +``` + +**Ejecutores clave**: `azure_content_understanding_extractor`, `azure_openai_agent`, `content_classifier` + +--- + +## 9. Procesamiento de adjuntos de correo electrónico + +Analiza hilos de correo para extraer tareas, detectar sentimiento y enrutar automáticamente a los equipos adecuados. + +**Capacidades**: + +| Industria | Aplicación | +|-----------|-----------| +| **Servicio al cliente** | Analizar hilos de soporte para extraer acciones pendientes, detectar sentimiento del cliente, y enrutar automáticamente problemas urgentes a los equipos apropiados | +| **Legal** | Categorizar automáticamente correspondencia legal, identificar puntos de negociación, extraer fechas límite y obligaciones de hilos de correo | + +**Pipeline sugerido**: + +```yaml +name: procesamiento-correos +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: correos-entrantes + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: sentiment_analyser + settings: + granularity: "documento" + + - executor: entity_extractor + settings: + entity_types: ["persona", "fecha", "organizacion", "accion"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1-mini + prompt: | + Del correo electrónico, extrae: + 1. Acciones pendientes y responsables + 2. Fechas límite mencionadas + 3. Nivel de urgencia (bajo, medio, alto, crítico) + 4. Tema principal y categoría + + - executor: content_classifier + settings: + categories: ["soporte_tecnico", "facturacion", "queja", + "solicitud_informacion", "legal"] + + - executor: azure_blob_output + settings: + blob_container_name: correos-procesados +``` + +**Ejecutores clave**: `sentiment_analyser`, `entity_extractor`, `azure_openai_agent`, `content_classifier` + +--- + +## 10. Ingestión de contenido para GPT-RAG + +Construye bases de conocimiento buscables a partir de documentos empresariales, procesando múltiples formatos y creando embeddings para búsqueda semántica en soluciones de chat con IA. + +**Capacidades**: +- Construir bases de conocimiento buscables desde documentos empresariales +- Procesar contenido en múltiples formatos para chat impulsado por IA +- Crear embeddings para búsqueda semántica + +**Pipeline sugerido**: + +```yaml +name: ingestion-gpt-rag +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: base-conocimiento + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + settings: + model_id: prebuilt-layout + + - executor: recursive_text_chunker + settings: + chunk_size: 800 + chunk_overlap: 150 + + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + - executor: gptrag_search_index_document_generator + settings: + index_name: rag-knowledge-base + + - executor: ai_search_index_output + settings: + index_name: rag-knowledge-base +``` + +**Ejecutores clave**: `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `gptrag_search_index_document_generator` + +--- + +## 11. Resumen de artículos y reportes + +Genera resúmenes concisos de artículos de noticias, reportes ejecutivos y snippets para redes sociales. + +**Capacidades**: +- Resumir artículos de noticias para boletines internos +- Crear resúmenes ejecutivos de reportes extensos +- Generar snippets para publicaciones en redes sociales + +**Pipeline sugerido**: + +```yaml +name: resumen-articulos +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: articulos-reportes + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: language_detector + + - executor: keyword_extractor + settings: + max_keywords: 10 + + - executor: text_summarizer + settings: + max_length: 300 + style: "resumen ejecutivo" + + - executor: azure_openai_agent + settings: + model: gpt-4.1-mini + prompt: | + A partir del resumen generado, crea: + 1. Un titular de una línea + 2. Un resumen de 3 oraciones para boletín + 3. Un snippet para redes sociales (max 280 caracteres) + + - executor: azure_blob_output + settings: + blob_container_name: resumenes-generados +``` + +**Ejecutores clave**: `keyword_extractor`, `text_summarizer`, `language_detector`, `azure_openai_agent` + +--- + +## 12. Traducción de contenido multilingüe + +Traduce documentación de productos, marketing y bases de conocimiento para alcanzar audiencias globales. + +**Capacidades**: +- Traducir documentación de productos a múltiples idiomas +- Localizar contenido de marketing para diferentes regiones +- Crear bases de conocimiento multilingües + +**Pipeline sugerido**: + +```yaml +name: traduccion-multilingue +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: contenido-original + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: language_detector + + - executor: recursive_text_chunker + settings: + chunk_size: 2000 + + - executor: content_translator + settings: + target_languages: ["es", "fr", "de", "pt", "ja"] + + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + - executor: ai_search_index_output + settings: + index_name: contenido-multilingue +``` + +**Ejecutores clave**: `language_detector`, `content_translator`, `azure_openai_embeddings`, `ai_search_index_output` + +--- + +## 13. Resumen de ejecutores por caso de uso + +| Caso de uso | Ejecutores principales | +|---|---| +| **Facturas y recibos** | `azure_content_understanding_extractor`, `content_classifier`, `field_mapper` | +| **Indexación inteligente** | `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `ai_search_index_output` | +| **Análisis de contratos** | `pdf_extractor`, `entity_extractor`, `azure_openai_agent`, `text_summarizer` | +| **Grafos de conocimiento** | `entity_extractor`, `azure_openai_agent`, `cross_document_comparison` | +| **Formularios** | `azure_content_understanding_extractor`, `field_selector`, `content_classifier` | +| **Cumplimiento y riesgos** | `pii_detector`, `content_classifier`, `azure_openai_agent` | +| **Contenido web** | `web_scraper`, `content_translator`, `azure_openai_embeddings` | +| **Análisis visual** | `azure_content_understanding_extractor`, `azure_openai_agent` | +| **Correo electrónico** | `sentiment_analyser`, `entity_extractor`, `content_classifier` | +| **GPT-RAG** | `azure_document_intelligence_extractor`, `recursive_text_chunker`, `gptrag_search_index_document_generator` | +| **Resúmenes** | `keyword_extractor`, `text_summarizer`, `azure_openai_agent` | +| **Traducción** | `language_detector`, `content_translator`, `azure_openai_embeddings` | + +--- + +## 14. Pipelines de ejemplo incluidos en el repositorio + +El repositorio incluye más de **20 pipelines de ejemplo** listos para ejecutar en la carpeta `contentflow-lib/samples/`: + +| Carpeta | Nombre | Descripción | +|---------|--------|-------------| +| `01-simple/` | Pipeline simple | Pipeline básico de extracción de texto | +| `02-batch-processing/` | Procesamiento por lotes | Procesamiento de múltiples documentos | +| `03-pdf-extractor_chunker/` | PDF + Chunking | Extracción de PDF y división en fragmentos | +| `04-word-extractor/` | Extractor Word | Procesamiento de documentos .docx | +| `05-powerpoint-extractor/` | Extractor PowerPoint | Procesamiento de presentaciones .pptx | +| `06-ai-analysis/` | Análisis con IA | Análisis inteligente con GPT | +| `07-embeddings/` | Embeddings | Generación de vectores de embedding | +| `08-content-understanding/` | Content Understanding | Azure Content Understanding | +| `09-blob-input/` | Entrada desde Blob | Descubrimiento de contenido en Blob Storage | +| `10-table-row-splitter/` | Divisor de filas | Procesamiento fila por fila de tablas | +| `11-excel-extractor/` | Extractor Excel | Procesamiento de hojas de cálculo | +| `12-field-transformation/` | Transformación de campos | Mapeo y transformación de datos | +| `13-blob-output-sample/` | Salida a Blob | Escritura de resultados a Blob Storage | +| `14-gpt-rag-ingestion/` | Ingestión GPT-RAG | Pipeline completo para RAG | +| `15-document-analysis/` | Análisis de documentos | Análisis completo con Document Intelligence | +| `16-spreadsheet-pipeline/` | Pipeline de hojas de cálculo | Procesamiento de Excel end-to-end | +| `17-knowledge-graph/` | Grafo de conocimiento | Extracción de entidades y relaciones | +| `18-web-scraping/` | Web scraping | Extracción de contenido web | +| `19-sub-pipelines/` | Sub-pipelines | Pipelines anidados | +| `20-document-set-static/` | Conjunto estático | Procesamiento de conjuntos de documentos | +| `21-document-set-comparison/` | Comparación de documentos | Comparación cruzada de documentos | +| `22-document-set-dynamic/` | Conjunto dinámico | Conjuntos de documentos dinámicos | +| `23-inline-document-set/` | Conjunto inline | Conjuntos definidos en línea | +| `27-subpipeline-processing/` | Sub-pipeline avanzado | Procesamiento con sub-pipelines | +| `28-advanced-batch/` | Lotes avanzado | Procesamiento por lotes avanzado | +| `32-parallel-processing/` | Procesamiento paralelo | Flujos de trabajo en paralelo | +| `44-conditional-routing/` | Enrutamiento condicional | Lógica condicional en pipelines | + +--- + +> Cada caso de uso se puede implementar combinando los **35+ ejecutores** disponibles en ContentFlow. Los pipelines YAML son completamente configurables y se pueden diseñar visualmente desde la interfaz web o editarse directamente en el editor YAML integrado. diff --git a/Analysis/04-ai_lz_options.md b/Analysis/04-ai_lz_options.md new file mode 100644 index 0000000..07aeb4d --- /dev/null +++ b/Analysis/04-ai_lz_options.md @@ -0,0 +1,1224 @@ +# Guía de Despliegue: ContentFlow en Modo AI Landing Zone Integrated + +> **Documento técnico operativo** - Instrucciones paso a paso para desplegar ContentFlow con conectividad privada usando una AI Landing Zone mínima en un ambiente de demo con suscripción única. + +--- + +## Tabla de Contenidos + +1. [Resumen Ejecutivo](#1-resumen-ejecutivo) +2. [¿Qué es el Modo AILZ-Integrated?](#2-qué-es-el-modo-ailz-integrated) +3. [Arquitectura de Red: Basic vs AILZ](#3-arquitectura-de-red-basic-vs-ailz) +4. [Prerrequisitos Generales](#4-prerrequisitos-generales) +5. [El Problema: Demo con Suscripción Única](#5-el-problema-demo-con-suscripción-única) +6. [Solución: AI Landing Zone Mínima ("Mini-AILZ")](#6-solución-ai-landing-zone-mínima-mini-ailz) +7. [Paso a Paso: Crear la Mini-AILZ](#7-paso-a-paso-crear-la-mini-ailz) +8. [Paso a Paso: Desplegar ContentFlow en Modo AILZ](#8-paso-a-paso-desplegar-contentflow-en-modo-ailz) +9. [Acceso y Pruebas de Conectividad Privada](#9-acceso-y-pruebas-de-conectividad-privada) +10. [Mapa Completo de Parámetros](#10-mapa-completo-de-parámetros) +11. [Cambios por Recurso en Modo AILZ](#11-cambios-por-recurso-en-modo-ailz) +12. [Troubleshooting](#12-troubleshooting) +13. [Costos Estimados de la Mini-AILZ](#13-costos-estimados-de-la-mini-ailz) +14. [Limpieza de Recursos](#14-limpieza-de-recursos) + +--- + +## 1. Resumen Ejecutivo + +ContentFlow soporta dos modos de despliegue: + +| Aspecto | `basic` | `ailz-integrated` | +|---|---|---| +| Endpoints | Públicos (internet) | Privados (VNet) | +| Red | Sin VNet | VNet + Private Endpoints | +| DNS | Resolución pública | Private DNS Zones | +| ACR SKU | Standard | **Premium** (requerido para PE) | +| Firewall de recursos | `Allow` (todo abierto) | `Deny` (solo PE) | +| Ingress de Container Apps | Externo (público) | **Interno** (solo VNet) | +| Acceso a la UI Web | Navegador directo | Requiere VPN/JumpBox | + +**Escenario de este documento:** Tienes **una sola suscripción de Azure** para demos y quieres probar el modo `ailz-integrated` completo. Normalmente, la AI Landing Zone ya existe como infraestructura compartida enterprise. Aquí crearemos una **versión mínima ("Mini-AILZ")** en la misma suscripción para simular ese escenario. + +--- + +## 2. ¿Qué es el Modo AILZ-Integrated? + +En un entorno enterprise real, el **AI Landing Zone** es una infraestructura de red pre-provisionada por el equipo de plataforma que incluye: + +- **Virtual Network (VNet)** con subredes dedicadas +- **Private DNS Zones** para resolución de nombres internos +- **Network Security Groups (NSGs)** y políticas de seguridad +- **JumpBox VM** para acceso administrativo +- **Shared Log Analytics** y **Application Insights** centralizados +- **Azure Firewall** o NVA para control de tráfico (enterprise completo) + +ContentFlow en modo `ailz-integrated` **no crea red propia**. En su lugar: + +1. **Reutiliza** la VNet y subredes existentes del AILZ +2. **Crea Private Endpoints** en la subred dedicada (`pe-subnet`) +3. **Registra registros DNS** en las Private DNS Zones existentes +4. **Configura todos los recursos** con `publicNetworkAccess: Disabled` +5. **Integra Container Apps Environment** con la VNet (modo interno) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ AI Landing Zone (VNet) │ +│ │ +│ ┌──────────────────────┐ ┌────────────────────────────────────┐ │ +│ │ pe-subnet (/27+) │ │ aca-env-subnet (/23) │ │ +│ │ │ │ │ │ +│ │ ● Storage PE (blob) │ │ ┌──────────────────────────────┐ │ │ +│ │ ● Storage PE (queue)│ │ │ Container Apps Environment │ │ │ +│ │ ● Cosmos DB PE │ │ │ (Internal Load Balancer) │ │ │ +│ │ ● App Config PE │ │ │ │ │ │ +│ │ ● ACR PE (Premium) │ │ │ ┌─────┐ ┌──────┐ ┌─────┐ │ │ │ +│ │ ● AI Foundry PE │ │ │ │ API │ │Worker│ │ Web │ │ │ │ +│ │ │ │ │ └─────┘ └──────┘ └─────┘ │ │ │ +│ └──────────────────────┘ │ └──────────────────────────────┘ │ │ +│ └────────────────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────────────────────────────────────┐ │ +│ │ jumpbox-vm │ │ Private DNS Zones (6 requeridas) │ │ +│ │ (acceso) │ │ ● privatelink.blob.core.windows.net │ │ +│ └──────────────┘ │ ● privatelink.documents.azure.com │ │ +│ │ ● privatelink.azconfig.io │ │ +│ │ ● privatelink.azurecr.io │ │ +│ │ ● privatelink.cognitiveservices.azure.com │ │ +│ │ ● privatelink.azurecontainerapps.io │ │ +│ └──────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. Arquitectura de Red: Basic vs AILZ + +### Modo Basic (Público) + +``` +Internet ──→ Container Apps (API/Web) ──→ Storage Account (público) + ──→ Cosmos DB (público) + ──→ App Config (público) + ──→ ACR Standard (público) + ──→ AI Foundry (público) +``` + +- Todos los recursos tienen endpoints públicos +- Accesibles desde cualquier IP +- Network ACLs: `defaultAction: Allow` + +### Modo AILZ-Integrated (Privado) + +``` +JumpBox/VPN ──→ VNet ──→ Container Apps (interno) ──→ Storage PE (privado) + ──→ Cosmos PE (privado) + ──→ App Config PE (privado) + ──→ ACR PE Premium (privado) + ──→ AI Foundry PE (privado) +``` + +- Todos los recursos: `publicNetworkAccess: Disabled` +- Network ACLs: `defaultAction: Deny`, `bypass: AzureServices` +- Acceso solo vía Private Endpoints dentro de la VNet +- Container Apps: `internal: true`, solo accesible desde VNet + +--- + +## 4. Prerrequisitos Generales + +### Herramientas Requeridas + +```bash +# Verificar instalación +az --version # Azure CLI 2.60+ +azd version # Azure Developer CLI 1.5+ +docker --version # Docker Desktop (para build de containers) +``` + +### Permisos Azure Requeridos + +| Permiso | Escenario | Razón | +|---|---|---| +| `Contributor` | Resource Group de ContentFlow | Crear todos los recursos | +| `Network Contributor` | Resource Group de la VNet | Crear Private Endpoints en subredes | +| `Private DNS Zone Contributor` | Resource Group de DNS Zones | Crear registros A para PEs | +| `User Access Administrator` | Resource Group | Asignar RBAC a Managed Identity | + +> **Nota:** En el escenario Mini-AILZ (suscripción única), si usas un Resource Group separado para la red, necesitas permisos en ambos RGs. + +### Registro de Providers + +```bash +# Asegurar que estos providers estén registrados +az provider register --namespace Microsoft.App +az provider register --namespace Microsoft.ContainerRegistry +az provider register --namespace Microsoft.DocumentDB +az provider register --namespace Microsoft.Network +az provider register --namespace Microsoft.CognitiveServices +az provider register --namespace Microsoft.Storage +az provider register --namespace Microsoft.AppConfiguration +``` + +--- + +## 5. El Problema: Demo con Suscripción Única + +En un entorno enterprise real: + +``` +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ Suscripción: Platform │ │ Suscripción: Workloads │ +│ │ │ │ +│ RG: rg-ailz-network │ │ RG: rg-contentflow │ +│ ├── VNet │ │ ├── Storage + PE │ +│ ├── Private DNS Zones │ │ ├── Cosmos DB + PE │ +│ ├── NSGs │ │ ├── Container Apps (int) │ +│ ├── Azure Firewall │ │ ├── ACR Premium + PE │ +│ └── JumpBox VM │ │ ├── App Config + PE │ +│ │ │ └── AI Foundry + PE │ +│ (Manejado por Platform Team)│ │ (Manejado por App Team) │ +└──────────────────────────────┘ └──────────────────────────────┘ +``` + +**Tu situación:** Solo tienes **una suscripción**. No existe un AILZ pre-provisionado. Necesitas: + +1. **Simular** la infraestructura de red que normalmente provee el Platform Team +2. **Desplegar** ContentFlow en modo `ailz-integrated` apuntando a esa red +3. **Acceder** a los servicios internos para validar la demo + +--- + +## 6. Solución: AI Landing Zone Mínima ("Mini-AILZ") + +Crearemos la infraestructura mínima necesaria en un **Resource Group separado** dentro de la misma suscripción: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Suscripción Única de Demo │ +│ │ +│ ┌──────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ RG: rg-mini-ailz │ │ RG: rg-contentflow-ailz │ │ +│ │ │ │ │ │ +│ │ ● VNet (10.0.0.0/16) │ │ ● Storage + PE │ │ +│ │ ├── pe-subnet /27 │ │ ● Cosmos DB + PE │ │ +│ │ ├── aca-env-subnet /23 │ │ ● ACR Premium + PE │ │ +│ │ └── jumpbox-subnet /27 │ │ ● App Config + PE │ │ +│ │ │ │ ● Container Apps Env (int) │ │ +│ │ ● 6 Private DNS Zones │ │ ● Container Apps (API/ │ │ +│ │ (vinculadas a la VNet) │ │ Worker/Web) │ │ +│ │ │ │ ● AI Foundry + PE │ │ +│ │ ● JumpBox VM (B2s) │ │ ● App Insights (nuevo) │ │ +│ │ (con Bastion o IP pública │ │ ● Log Analytics (nuevo) │ │ +│ │ temporal para demo) │ │ ● User Assigned Identity │ │ +│ │ │ │ │ │ +│ │ ● Log Analytics (opcional) │ │ │ │ +│ │ ● App Insights (opcional) │ │ │ │ +│ └──────────────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### ¿Por qué un RG separado? + +- **Simula el escenario real**: Red y aplicación en RGs diferentes +- **Limpieza fácil**: Puedes borrar `rg-contentflow-ailz` sin afectar la red +- **Permisos realistas**: Puedes validar que los permisos cross-RG funcionan +- **Reutilizable**: La Mini-AILZ puede servir para otras demos de servicios con Private Endpoints + +--- + +## 7. Paso a Paso: Crear la Mini-AILZ + +### 7.1 Definir Variables + +```bash +# === CONFIGURACIÓN === +# Ajusta estos valores a tu ambiente +SUBSCRIPTION_ID=$(az account show --query id -o tsv) +LOCATION="eastus2" # Tu región preferida +AILZ_RG="rg-mini-ailz" # RG para la infraestructura de red +CF_RG="rg-contentflow-ailz" # RG para ContentFlow (lo crea azd) +VNET_NAME="vnet-ailz-demo" +VNET_PREFIX="10.0.0.0/16" + +# Subredes +PE_SUBNET_NAME="pe-subnet" # Nombre esperado por ContentFlow +PE_SUBNET_PREFIX="10.0.1.0/27" # /27 = 32 IPs (suficiente para ~25 PEs) +ACA_SUBNET_NAME="aca-env-subnet" # Nombre esperado por ContentFlow +ACA_SUBNET_PREFIX="10.0.16.0/23" # /23 = 512 IPs (requerido por Container Apps) +JUMPBOX_SUBNET_NAME="jumpbox-subnet" +JUMPBOX_SUBNET_PREFIX="10.0.2.0/27" # /27 = 32 IPs +``` + +### 7.2 Crear Resource Group de Red + +```bash +az group create \ + --name $AILZ_RG \ + --location $LOCATION \ + --tags purpose=mini-ailz owner=demo +``` + +### 7.3 Crear Virtual Network con Subredes + +```bash +# Crear VNet +az network vnet create \ + --resource-group $AILZ_RG \ + --name $VNET_NAME \ + --address-prefix $VNET_PREFIX \ + --location $LOCATION \ + --tags purpose=mini-ailz + +# Subred para Private Endpoints +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name $PE_SUBNET_NAME \ + --address-prefix $PE_SUBNET_PREFIX + +# Subred para Container Apps Environment +# IMPORTANTE: Container Apps necesita /23 mínimo y la subred debe estar delegada +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name $ACA_SUBNET_NAME \ + --address-prefix $ACA_SUBNET_PREFIX + +# Subred para JumpBox +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name $JUMPBOX_SUBNET_NAME \ + --address-prefix $JUMPBOX_SUBNET_PREFIX +``` + +> **Nota sobre el tamaño de subred para Container Apps:** Container Apps Environment en modo VNet-integrated requiere una subred de al menos `/23` (512 direcciones). Esto es un requisito de la plataforma para manejar la infraestructura interna del entorno. + +### 7.4 Crear las 6 Private DNS Zones Requeridas + +Cada zona debe vincularse (link) a la VNet para que la resolución DNS funcione: + +```bash +# Lista de zonas requeridas por ContentFlow +DNS_ZONES=( + "privatelink.blob.core.windows.net" + "privatelink.documents.azure.com" + "privatelink.azconfig.io" + "privatelink.azurecr.io" + "privatelink.cognitiveservices.azure.com" + "privatelink.azurecontainerapps.io" +) + +# Crear cada zona y vincularla a la VNet +for ZONE in "${DNS_ZONES[@]}"; do + echo "Creando DNS Zone: $ZONE" + + # Crear la zona + az network private-dns zone create \ + --resource-group $AILZ_RG \ + --name "$ZONE" \ + --tags purpose=mini-ailz + + # Vincular la zona a la VNet (CRÍTICO - sin esto la resolución DNS no funciona) + LINK_NAME=$(echo "$ZONE" | sed 's/\./-/g')-link + az network private-dns link vnet create \ + --resource-group $AILZ_RG \ + --zone-name "$ZONE" \ + --name "$LINK_NAME" \ + --virtual-network "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" \ + --registration-enabled false +done + +echo "✓ 6 Private DNS Zones creadas y vinculadas a la VNet" +``` + +### 7.5 (Opcional) Crear Private DNS Zone para Key Vault + +```bash +# Solo si planeas usar Key Vault con Private Endpoint +az network private-dns zone create \ + --resource-group $AILZ_RG \ + --name "privatelink.vaultcore.azure.net" \ + --tags purpose=mini-ailz + +az network private-dns link vnet create \ + --resource-group $AILZ_RG \ + --zone-name "privatelink.vaultcore.azure.net" \ + --name "privatelink-vaultcore-azure-net-link" \ + --virtual-network "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" \ + --registration-enabled false +``` + +### 7.6 Crear JumpBox VM + +La JumpBox es necesaria para acceder a los servicios internos después del despliegue. Tienes **tres opciones**: + +#### Opción A: JumpBox con Azure Bastion (Recomendada para Demo) + +Azure Bastion provee acceso seguro RDP/SSH sin exponer IP pública en la VM. + +```bash +# Crear subred para Azure Bastion (REQUIERE nombre exacto y mínimo /26) +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name "AzureBastionSubnet" \ + --address-prefix "10.0.3.0/26" + +# Crear IP pública para Bastion +az network public-ip create \ + --resource-group $AILZ_RG \ + --name "pip-bastion-demo" \ + --sku Standard \ + --allocation-method Static \ + --location $LOCATION + +# Crear Azure Bastion (Developer SKU - más económico) +az network bastion create \ + --resource-group $AILZ_RG \ + --name "bastion-demo" \ + --public-ip-address "pip-bastion-demo" \ + --vnet-name $VNET_NAME \ + --sku Developer \ + --location $LOCATION + +# Crear la JumpBox VM (sin IP pública - Bastion proporciona acceso) +az vm create \ + --resource-group $AILZ_RG \ + --name "jmp-ailz-demo" \ + --image "MicrosoftWindowsDesktop:windows-11:win11-24h2-pro:latest" \ + --size "Standard_B2ms" \ + --vnet-name $VNET_NAME \ + --subnet $JUMPBOX_SUBNET_NAME \ + --public-ip-address "" \ + --admin-username "azureuser" \ + --admin-password "$(openssl rand -base64 16)A1!" \ + --tags role=jumpbox purpose=mini-ailz \ + --location $LOCATION +``` + +> **Importante:** Anota la contraseña generada. También puedes usar `--admin-password` con un valor específico que controles. + +#### Opción B: JumpBox con IP Pública Temporal (Más Simple) + +```bash +# JumpBox con IP pública (solo para demo, desactivar después) +az vm create \ + --resource-group $AILZ_RG \ + --name "jmp-ailz-demo" \ + --image "MicrosoftWindowsDesktop:windows-11:win11-24h2-pro:latest" \ + --size "Standard_B2ms" \ + --vnet-name $VNET_NAME \ + --subnet $JUMPBOX_SUBNET_NAME \ + --admin-username "azureuser" \ + --admin-password "TuPasswordSeguro123!" \ + --tags role=jumpbox purpose=mini-ailz \ + --nsg-rule RDP \ + --location $LOCATION +``` + +> **Seguridad:** Restringe RDP solo a tu IP: +> ```bash +> MY_IP=$(curl -s https://api.ipify.org) +> az network nsg rule update \ +> --resource-group $AILZ_RG \ +> --nsg-name "jmp-ailz-demoNSG" \ +> --name "rdp" \ +> --source-address-prefixes "$MY_IP/32" +> ``` + +#### Opción C: JumpBox Linux (Más Económica) + +```bash +az vm create \ + --resource-group $AILZ_RG \ + --name "jmp-ailz-demo" \ + --image "Ubuntu2404" \ + --size "Standard_B2s" \ + --vnet-name $VNET_NAME \ + --subnet $JUMPBOX_SUBNET_NAME \ + --admin-username "azureuser" \ + --generate-ssh-keys \ + --tags role=jumpbox purpose=mini-ailz \ + --public-ip-address "" \ + --location $LOCATION +``` + +### 7.7 (Opcional) Crear Observabilidad Compartida + +Si quieres simular el escenario enterprise donde Log Analytics y App Insights son compartidos: + +```bash +# Log Analytics Workspace compartido +az monitor log-analytics workspace create \ + --resource-group $AILZ_RG \ + --workspace-name "log-ailz-shared" \ + --sku PerGB2018 \ + --location $LOCATION \ + --tags purpose=mini-ailz + +# Application Insights compartido +LOG_WS_ID=$(az monitor log-analytics workspace show \ + --resource-group $AILZ_RG \ + --workspace-name "log-ailz-shared" \ + --query id -o tsv) + +az monitor app-insights component create \ + --resource-group $AILZ_RG \ + --app "appi-ailz-shared" \ + --location $LOCATION \ + --workspace "$LOG_WS_ID" \ + --tags purpose=mini-ailz +``` + +> **Nota:** Si no creas estos recursos, ContentFlow en modo `ailz-integrated` **creará sus propios** Log Analytics y App Insights (el código Bicep lo contempla). Solo necesitas crearlos si quieres probar el escenario de observabilidad compartida. + +### 7.8 Verificar la Mini-AILZ + +```bash +echo "=== Verificación de Mini-AILZ ===" + +echo "VNet:" +az network vnet show -g $AILZ_RG -n $VNET_NAME --query "{name:name, address:addressSpace.addressPrefixes[0]}" -o table + +echo "" +echo "Subredes:" +az network vnet subnet list -g $AILZ_RG --vnet-name $VNET_NAME --query "[].{name:name, prefix:addressPrefix}" -o table + +echo "" +echo "Private DNS Zones:" +az network private-dns zone list -g $AILZ_RG --query "[].{name:name, links:numberOfVirtualNetworkLinks}" -o table + +echo "" +echo "JumpBox VM:" +az vm list -g $AILZ_RG --query "[].{name:name, size:hardwareProfile.vmSize, tags:tags.role}" -o table +``` + +Salida esperada: +``` +=== Verificación de Mini-AILZ === +VNet: +Name Address +----------- ---------- +vnet-ailz-demo 10.0.0.0/16 + +Subredes: +Name Prefix +----------- ---------- +pe-subnet 10.0.1.0/27 +aca-env-subnet 10.0.16.0/23 +jumpbox-subnet 10.0.2.0/27 + +Private DNS Zones: +Name Links +--------------------------------------------- ----- +privatelink.blob.core.windows.net 1 +privatelink.documents.azure.com 1 +privatelink.azconfig.io 1 +privatelink.azurecr.io 1 +privatelink.cognitiveservices.azure.com 1 +privatelink.azurecontainerapps.io 1 + +JumpBox VM: +Name Size Tags +----------- ----------- -------- +jmp-ailz-demo Standard_B2ms jumpbox +``` + +--- + +## 8. Paso a Paso: Desplegar ContentFlow en Modo AILZ + +### 8.1 Obtener IDs de Recursos de la Mini-AILZ + +Usa el script incluido en ContentFlow o hazlo manualmente: + +#### Opción A: Script Automático (Recomendada) + +```bash +cd infra/scripts + +# Ejecutar con auto-set (configura variables de azd automáticamente) +./get-ailz-resources.sh --auto-set +# Cuando pregunte por el RG, ingresa: rg-mini-ailz +``` + +El script busca automáticamente: +- VNet y subredes (`pe-subnet`, `aca-env-subnet`) +- Las 6 Private DNS Zones requeridas +- JumpBox VM (busca tag `role=jumpbox` o nombre con `jmp`) +- Log Analytics y App Insights (opcionales) + +Genera un archivo `ailz-resources.env` y con `--auto-set` configura las variables en azd directamente. + +#### Opción B: Recopilación Manual + +```bash +# Obtener VNet Resource ID +VNET_ID=$(az network vnet show -g $AILZ_RG -n $VNET_NAME --query id -o tsv) +echo "VNET_ID: $VNET_ID" + +# Obtener IDs de Private DNS Zones +BLOB_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.blob.core.windows.net" --query id -o tsv) +COSMOS_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.documents.azure.com" --query id -o tsv) +APPCONFIG_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azconfig.io" --query id -o tsv) +ACR_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azurecr.io" --query id -o tsv) +COGNITIVE_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.cognitiveservices.azure.com" --query id -o tsv) +ACA_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azurecontainerapps.io" --query id -o tsv) + +# Opcionales +LOG_WS_ID=$(az monitor log-analytics workspace show -g $AILZ_RG -n "log-ailz-shared" --query id -o tsv 2>/dev/null || echo "") +APPI_ID=$(az monitor app-insights component show -g $AILZ_RG --app "appi-ailz-shared" --query id -o tsv 2>/dev/null || echo "") +``` + +### 8.2 Inicializar Entorno de azd + +```bash +# Ir al raíz del repositorio +cd /ruta/a/contentflow + +# Inicializar nuevo entorno azd +azd init -e contentflow-ailz-demo + +# Configurar ubicación y suscripción +azd env set AZURE_LOCATION "$LOCATION" +azd env set AZURE_AI_FOUNDRY_LOCATION "$LOCATION" +``` + +### 8.3 Configurar Variables de Modo AILZ + +```bash +# === MODO DE DESPLIEGUE === +azd env set DEPLOYMENT_MODE "ailz-integrated" + +# === RED === +azd env set EXISTING_VNET_RESOURCE_ID "$VNET_ID" +azd env set PRIVATE_ENDPOINT_SUBNET_NAME "pe-subnet" +azd env set CONTAINER_APPS_SUBNET_NAME "aca-env-subnet" + +# === PRIVATE DNS ZONES (6 requeridas) === +azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID "$BLOB_DNS_ID" +azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID "$COSMOS_DNS_ID" +azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID "$APPCONFIG_DNS_ID" +azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID "$ACR_DNS_ID" +azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID "$COGNITIVE_DNS_ID" +azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$ACA_DNS_ID" + +# === OPCIONALES: OBSERVABILIDAD COMPARTIDA === +# Solo si creaste Log Analytics y App Insights compartidos en el paso 7.7 +if [ ! -z "$LOG_WS_ID" ]; then + azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$LOG_WS_ID" +fi +if [ ! -z "$APPI_ID" ]; then + azd env set EXISTING_APP_INSIGHTS_ID "$APPI_ID" +fi + +# === Principal ID (tu usuario para RBAC) === +PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv) +azd env set AZURE_PRINCIPAL_ID "$PRINCIPAL_ID" +``` + +### 8.4 Verificar Configuración + +```bash +# Listar todas las variables del entorno azd +azd env get-values | grep -E "DEPLOYMENT_MODE|EXISTING_|PRIVATE_|CONTAINER_APPS_SUBNET|AZURE_LOCATION" +``` + +Salida esperada: +``` +DEPLOYMENT_MODE="ailz-integrated" +EXISTING_VNET_RESOURCE_ID="/subscriptions/.../virtualNetworks/vnet-ailz-demo" +PRIVATE_ENDPOINT_SUBNET_NAME="pe-subnet" +CONTAINER_APPS_SUBNET_NAME="aca-env-subnet" +EXISTING_BLOB_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.blob.core.windows.net" +EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.documents.azure.com" +EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azconfig.io" +EXISTING_ACR_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azurecr.io" +EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.cognitiveservices.azure.com" +EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azurecontainerapps.io" +AZURE_LOCATION="eastus2" +``` + +### 8.5 Desplegar + +```bash +# Despliegue completo (provision + build + deploy) +azd up +``` + +**¿Qué sucede durante `azd up`?** + +1. **Pre-provision hook**: Valida que Azure CLI, azd y Docker estén instalados +2. **Bicep Provisioning**: + - Valida parámetros AILZ (all 6 DNS zones + VNet + subredes) + - Crea User Assigned Identity + - Crea Storage Account con PE en `pe-subnet` → registra en `privatelink.blob.core.windows.net` + - Crea Cosmos DB con PE en `pe-subnet` → registra en `privatelink.documents.azure.com` + - Crea App Configuration con PE en `pe-subnet` → registra en `privatelink.azconfig.io` + - Crea Container Registry **Premium** con PE en `pe-subnet` → registra en `privatelink.azurecr.io` + - Crea Container Apps Environment **interno** en `aca-env-subnet` + - Crea Container Apps (API, Worker, Web) con ingress **interno** + - Crea AI Foundry Hub & Project + - Si no proporcionaste Log Analytics/App Insights existentes: crea nuevos + - Configura RBAC para la Managed Identity +3. **Container Build**: Construye imágenes Docker para API, Worker, Web +4. **Push to ACR**: Sube imágenes a Container Registry (vía PE si estás en la VNet, o vía Azure CLI si públicamente) +5. **Deploy to Container Apps**: Despliega las apps (internamente accesibles) +6. **Post-deploy hook**: Muestra endpoints (internos) + +> **⚠️ IMPORTANTE sobre el push a ACR:** En modo AILZ, el ACR tiene `publicNetworkAccess: Disabled`. Si ejecutas `azd up` desde tu máquina local (fuera de la VNet), el push de imágenes **podría fallar**. Ver sección de Troubleshooting para soluciones. + +### 8.6 Verificar Despliegue + +```bash +# Verificar que los Private Endpoints se crearon correctamente +az network private-endpoint list \ + --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) \ + --query "[].{name:name, status:privateLinkServiceConnections[0].privateLinkServiceConnectionState.status}" \ + -o table +``` + +Salida esperada: +``` +Name Status +----------------------- --------- +stXXXXX-blob-pe Approved +stXXXXX-queue-pe Approved +cosmos-XXXXX-pe Approved +appcs-XXXXX-pe Approved +crXXXXX-pe Approved +``` + +--- + +## 9. Acceso y Pruebas de Conectividad Privada + +### 9.1 Acceder vía JumpBox + +Dado que todos los servicios son internos, necesitas estar dentro de la VNet: + +```bash +# Conectar a la JumpBox vía Bastion (Portal Azure) +# O vía Azure CLI: +az network bastion ssh \ + --resource-group $AILZ_RG \ + --name "bastion-demo" \ + --target-resource-id $(az vm show -g $AILZ_RG -n "jmp-ailz-demo" --query id -o tsv) \ + --auth-type password \ + --username azureuser +``` + +### 9.2 Conseguir URLs Internas + +Las URLs de Container Apps en modo interno tienen el formato: +``` +https://...azurecontainerapps.io +``` + +Pero solo son resolvibles dentro de la VNet (gracias a la Private DNS Zone `privatelink.azurecontainerapps.io`). + +```bash +# Obtener URLs internas desde azd +azd env get-values | grep "SERVICE_.*_URI" + +# O directamente: +API_URL=$(az containerapp show -g $(azd env get-value AZURE_RESOURCE_GROUP) -n $(azd env get-value API_CONTAINER_APP_NAME) --query "properties.configuration.ingress.fqdn" -o tsv) +WEB_URL=$(az containerapp show -g $(azd env get-value AZURE_RESOURCE_GROUP) -n $(azd env get-value WEB_CONTAINER_APP_NAME) --query "properties.configuration.ingress.fqdn" -o tsv) + +echo "API: https://$API_URL" +echo "Web: https://$WEB_URL" +``` + +### 9.3 Probar desde la JumpBox + +Desde la JumpBox (Windows): +```powershell +# Verificar resolución DNS (debe resolver a IP privada 10.x.x.x) +nslookup + +# Probar API health check +Invoke-WebRequest -Uri "https:///health" -UseBasicParsing + +# Abrir navegador para la Web UI +Start-Process "https://" +``` + +Desde la JumpBox (Linux): +```bash +# Verificar resolución DNS +nslookup +# Debe retornar IP privada (10.x.x.x), NO una IP pública + +# Probar API +curl -k https:///health + +# Probar que los Private Endpoints resuelven correctamente +nslookup .blob.core.windows.net +# Debe retornar privatelink.blob.core.windows.net → 10.x.x.x + +nslookup .documents.azure.com +# Debe retornar privatelink.documents.azure.com → 10.x.x.x +``` + +### 9.4 Probar Resolución DNS de Cada Servicio + +```bash +# Desde la JumpBox, verificar que todos los PEs resuelven a IPs privadas +SERVICES=( + ".blob.core.windows.net" + ".queue.core.windows.net" + ".documents.azure.com" + ".azconfig.io" + ".azurecr.io" +) + +for SVC in "${SERVICES[@]}"; do + echo "=== $SVC ===" + nslookup $SVC + echo "" +done +``` + +--- + +## 10. Mapa Completo de Parámetros + +### Variables de Entorno azd → Parámetros Bicep + +| Variable azd | Parámetro Bicep | Requerido en AILZ | Default | Descripción | +|---|---|---|---|---| +| `DEPLOYMENT_MODE` | `deploymentMode` | ✅ | — | Debe ser `ailz-integrated` | +| `AZURE_ENV_NAME` | `environmentName` | ✅ | — | Nombre del entorno | +| `AZURE_LOCATION` | `location` | ✅ | — | Región Azure | +| `AZURE_AI_FOUNDRY_LOCATION` | `foundryLocation` | ✅ | — | Región para AI Foundry | +| `AZURE_PRINCIPAL_ID` | `principalId` | ⬜ | `""` | Tu Object ID para RBAC | +| `EXISTING_VNET_RESOURCE_ID` | `existingVnetResourceId` | ✅ | `""` | Resource ID completo de la VNet | +| `PRIVATE_ENDPOINT_SUBNET_NAME` | `privateEndpointSubnetName` | ✅ | `pe-subnet` | Nombre de la subred para PEs | +| `CONTAINER_APPS_SUBNET_NAME` | `containerAppsSubnetName` | ✅ | `aca-env-subnet` | Nombre de la subred para CAE | +| `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | `existingCognitiveServicesPrivateDnsZoneId` | ✅ | `""` | DNS Zone para AI Services | +| `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | `existingBlobPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Blob Storage | +| `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | `existingCosmosPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Cosmos DB | +| `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | `existingAppConfigPrivateDnsZoneId` | ✅ | `""` | DNS Zone para App Config | +| `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | `existingAcrPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Container Registry | +| `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | `existingContainerAppsEnvPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Container Apps | +| `EXISTING_KEY_VAULT_PRIVATE_DNS_ZONE_ID` | `existingKeyVaultPrivateDnsZoneId` | ⬜ | `""` | DNS Zone para Key Vault | +| `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | `existingLogAnalyticsWorkspaceId` | ⬜ | `""` | Log Analytics compartido | +| `EXISTING_APP_INSIGHTS_ID` | `existingAppInsightsId` | ⬜ | `""` | App Insights compartido | + +### Formato de Resource IDs + +``` +VNet: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{name} +DNS Zone: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/{zone-name} +Log Analyt: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{name} +App Insigh: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Insights/components/{name} +``` + +--- + +## 11. Cambios por Recurso en Modo AILZ + +### Comparativa Detallada + +| Recurso | Basic | AILZ-Integrated | +|---|---|---| +| **Storage Account** | SKU: Standard_LRS, Public | SKU: Standard_LRS, **PE (blob+queue)**, `Deny` ACL | +| **Cosmos DB** | Serverless, Public | Serverless, **PE**, `Disabled` public access | +| **App Configuration** | Standard, Public | Standard, **PE**, `Disabled` public access | +| **Container Registry** | **Standard** | **Premium** (requerido para PE), **PE** | +| **Container Apps Env** | Public, External LB | **VNet-integrated**, **Internal LB**, `aca-env-subnet` | +| **Container Apps (API)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | +| **Container Apps (Worker)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | +| **Container Apps (Web)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | +| **AI Foundry** | Public | **PE** para Cognitive Services | +| **Log Analytics** | Creado nuevo (basic) | Usa existente O crea nuevo | +| **App Insights** | Creado nuevo (basic) | Usa existente O crea nuevo | +| **Managed Identity** | Sin cambios | Sin cambios | + +### Nomenclatura de Private Endpoints + +ContentFlow usa una convención consistente: + +``` +{resourceName}-{service}-pe → Nombre del Private Endpoint +{resourceName}-{service}-plsc → Private Link Service Connection +{service}-dns-zone-group → DNS Zone Group +{service}-config → DNS Zone Group Config +``` + +Ejemplos: +- `st4a7b2c-blob-pe` / `st4a7b2c-blob-plsc` +- `st4a7b2c-queue-pe` / `st4a7b2c-queue-plsc` +- `cosmos-4a7b2c-pe` / `cosmos-4a7b2c-cosmos-plsc` +- `appcs-4a7b2c-pe` / `appcs-4a7b2c-app-config-plsc` +- `cr4a7b2c-pe` / `cr4a7b2c-acr-plsc` + +--- + +## 12. Troubleshooting + +### Problema 1: Error "ACR push failed" durante `azd up` + +**Causa:** ACR tiene `publicNetworkAccess: Disabled`. Tu máquina local no puede hacer push de imágenes. + +**Soluciones:** + +**Solución A - Temporalmente habilitar acceso público al ACR:** +```bash +# Antes de azd deploy +ACR_NAME=$(azd env get-value AZURE_CONTAINER_REGISTRY_NAME) +CF_RG=$(azd env get-value AZURE_RESOURCE_GROUP) + +# Habilitar temporalmente +az acr update -n $ACR_NAME -g $CF_RG --public-network-enabled true + +# Hacer el deploy +azd deploy + +# Volver a deshabilitar +az acr update -n $ACR_NAME -g $CF_RG --public-network-enabled false +``` + +**Solución B - Build y push desde la JumpBox:** +```bash +# Ejecutar azd up desde la JumpBox (dentro de la VNet) +# Requiere instalar azd, az cli, docker en la JumpBox +``` + +**Solución C - Usar ACR Tasks (build en la nube):** +```bash +# ACR Tasks puede hacer build sin necesidad de Docker local +az acr build --registry $ACR_NAME -t contentflow-api:latest ./contentflow-api/ +az acr build --registry $ACR_NAME -t contentflow-worker:latest ./contentflow-worker/ +az acr build --registry $ACR_NAME -t contentflow-web:latest ./contentflow-web/ +``` + +### Problema 2: Error "VNet subnet not found" + +**Causa:** Los nombres de subred no coinciden con los esperados. + +**Solución:** +```bash +# Verificar nombres exactos +az network vnet subnet list -g $AILZ_RG --vnet-name $VNET_NAME --query "[].name" -o tsv + +# Si los nombres son diferentes, ajustar: +azd env set PRIVATE_ENDPOINT_SUBNET_NAME "tu-nombre-de-pe-subnet" +azd env set CONTAINER_APPS_SUBNET_NAME "tu-nombre-de-aca-subnet" +``` + +### Problema 3: Error de validación "fail('existingXXX is required')" + +**Causa:** Falta algún parámetro requerido para modo `ailz-integrated`. + +**Solución:** Ejecutar el script de verificación: +```bash +# Verificar todas las variables están configuradas +REQUIRED_VARS=( + "DEPLOYMENT_MODE" + "EXISTING_VNET_RESOURCE_ID" + "EXISTING_BLOB_PRIVATE_DNS_ZONE_ID" + "EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID" + "EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID" + "EXISTING_ACR_PRIVATE_DNS_ZONE_ID" + "EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID" + "EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID" +) + +for VAR in "${REQUIRED_VARS[@]}"; do + VALUE=$(azd env get-value $VAR 2>/dev/null) + if [ -z "$VALUE" ]; then + echo "❌ FALTA: $VAR" + else + echo "✓ $VAR configurada" + fi +done +``` + +### Problema 4: DNS no resuelve a IP privada desde JumpBox + +**Causa:** La Private DNS Zone no está vinculada a la VNet, o el DNS Zone Group no se creó. + +**Solución:** +```bash +# Verificar que cada zona tiene un VNet link +for ZONE in "privatelink.blob.core.windows.net" "privatelink.documents.azure.com" "privatelink.azconfig.io" "privatelink.azurecr.io" "privatelink.cognitiveservices.azure.com" "privatelink.azurecontainerapps.io"; do + echo "=== $ZONE ===" + az network private-dns link vnet list --zone-name $ZONE -g $AILZ_RG --query "[].{name:name, state:virtualNetworkLinkState}" -o table +done + +# Si falta un link: +az network private-dns link vnet create \ + --resource-group $AILZ_RG \ + --zone-name "privatelink.blob.core.windows.net" \ + --name "blob-vnet-link" \ + --virtual-network "$VNET_ID" \ + --registration-enabled false +``` + +### Problema 5: Container Apps no inician (health check failed) + +**Causa:** Las Container Apps no pueden alcanzar los servicios backend (Storage, Cosmos) porque los PEs no están configurados correctamente o los DNS Zone Groups no registraron los records A. + +**Solución:** +```bash +# Verificar logs de Container Apps +az containerapp logs show \ + -n $(azd env get-value API_CONTAINER_APP_NAME) \ + -g $(azd env get-value AZURE_RESOURCE_GROUP) \ + --type system + +# Verificar que los A records existen en las DNS zones +az network private-dns record-set a list -g $AILZ_RG -z "privatelink.blob.core.windows.net" -o table +az network private-dns record-set a list -g $AILZ_RG -z "privatelink.documents.azure.com" -o table +``` + +### Problema 6: Permisos insuficientes para crear PE en subnet de otro RG + +**Causa:** Tu usuario no tiene `Network Contributor` en el RG de la VNet. + +**Solución:** +```bash +# Asignar permisos en el RG de red +USER_OBJ_ID=$(az ad signed-in-user show --query id -o tsv) + +az role assignment create \ + --assignee $USER_OBJ_ID \ + --role "Network Contributor" \ + --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG" + +az role assignment create \ + --assignee $USER_OBJ_ID \ + --role "Private DNS Zone Contributor" \ + --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG" +``` + +### Problema 7: Error "Container Apps Environment subnet too small" + +**Causa:** La subred `aca-env-subnet` es menor a `/23`. + +**Solución:** Recrear la subred con el tamaño correcto (requiere borrar la existente primero si no tiene recursos): +```bash +az network vnet subnet delete -g $AILZ_RG --vnet-name $VNET_NAME -n $ACA_SUBNET_NAME +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name $ACA_SUBNET_NAME \ + --address-prefix "10.0.16.0/23" +``` + +--- + +## 13. Costos Estimados de la Mini-AILZ + +### Recursos de Red (RG: rg-mini-ailz) + +| Recurso | SKU/Tier | Costo Estimado (USD/mes) | +|---|---|---| +| VNet + Subredes | Gratuito | $0 | +| Private DNS Zones (6) | $0.50/zona | ~$3 | +| Azure Bastion | Developer SKU | ~$5.50 | +| JumpBox VM (B2ms, Windows) | Standard_B2ms | ~$60 (24/7) | +| JumpBox VM (B2s, Linux) | Standard_B2s | ~$15 (24/7) | + +**Tip para reducir costos de demo:** +```bash +# Apagar la JumpBox cuando no la uses +az vm deallocate -g $AILZ_RG -n "jmp-ailz-demo" + +# Encenderla cuando la necesites +az vm start -g $AILZ_RG -n "jmp-ailz-demo" +``` + +### Recursos ContentFlow Adicionales en AILZ vs Basic + +| Recurso | Cambio en AILZ | Impacto en Costo | +|---|---|---| +| Container Registry | Standard → **Premium** | +~$45/mes | +| Private Endpoints (5-6) | Cada PE tiene costo | +~$5/mes total | +| Procesamiento de PE (datos) | Por GB procesado | Mínimo en demo | + +### Costo Total Estimado de Demo (Mini-AILZ + ContentFlow) + +| Componente | Costo Estimado | +|---|---| +| Mini-AILZ (red + JumpBox Linux + Bastion) | ~$24/mes | +| ContentFlow AILZ (Premium ACR, PEs) delta vs Basic | ~$50/mes | +| ContentFlow base (Storage, Cosmos, Container Apps, AI) | ~$30-80/mes | +| **Total estimado** | **~$100-150/mes** | + +> **Nota:** Los costos de AI Foundry (GPT-4.1) son por uso (tokens). El estimado base asume uso mínimo. Apaga la JumpBox cuando no la uses para reducir costos. + +--- + +## 14. Limpieza de Recursos + +### Eliminar ContentFlow + +```bash +# Eliminar todos los recursos de ContentFlow +azd down --force --purge +``` + +> `--purge` elimina los recursos en soft-delete (Cosmos DB, Key Vault, AI Services). + +### Eliminar la Mini-AILZ + +```bash +# Eliminar todo el Resource Group de red +az group delete --name $AILZ_RG --yes --no-wait +``` + +### Limpieza Selectiva (Mantener la Red) + +Si quieres mantener la Mini-AILZ para futuras demos: +```bash +# Solo eliminar ContentFlow +azd down --force --purge + +# Apagar JumpBox para ahorrar +az vm deallocate -g $AILZ_RG -n "jmp-ailz-demo" +``` + +--- + +## Apéndice A: Script Completo de Creación de Mini-AILZ + +Para conveniencia, aquí está el script completo que crea toda la Mini-AILZ de un solo paso: + +```bash +#!/bin/bash +# create-mini-ailz.sh - Crea una AI Landing Zone mínima para demo de ContentFlow +set -e + +# === CONFIGURACIÓN (AJUSTAR SEGÚN TU AMBIENTE) === +SUBSCRIPTION_ID=$(az account show --query id -o tsv) +LOCATION="eastus2" +AILZ_RG="rg-mini-ailz" +VNET_NAME="vnet-ailz-demo" +VNET_PREFIX="10.0.0.0/16" +PE_SUBNET_PREFIX="10.0.1.0/27" +ACA_SUBNET_PREFIX="10.0.16.0/23" +JUMPBOX_SUBNET_PREFIX="10.0.2.0/27" +BASTION_SUBNET_PREFIX="10.0.3.0/26" + +echo "=== Creando Mini-AILZ para ContentFlow Demo ===" +echo "Suscripción: $SUBSCRIPTION_ID" +echo "Ubicación: $LOCATION" +echo "Resource Group: $AILZ_RG" +echo "" + +# 1. Resource Group +echo "[1/6] Creando Resource Group..." +az group create -n $AILZ_RG -l $LOCATION --tags purpose=mini-ailz -o none + +# 2. VNet + Subredes +echo "[2/6] Creando VNet y subredes..." +az network vnet create -g $AILZ_RG -n $VNET_NAME --address-prefix $VNET_PREFIX -l $LOCATION -o none +az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "pe-subnet" --address-prefix $PE_SUBNET_PREFIX -o none +az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "aca-env-subnet" --address-prefix $ACA_SUBNET_PREFIX -o none +az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "jumpbox-subnet" --address-prefix $JUMPBOX_SUBNET_PREFIX -o none +az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "AzureBastionSubnet" --address-prefix $BASTION_SUBNET_PREFIX -o none +echo " ✓ VNet con 4 subredes creada" + +# 3. Private DNS Zones +echo "[3/6] Creando Private DNS Zones..." +VNET_ID="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" +DNS_ZONES=( + "privatelink.blob.core.windows.net" + "privatelink.documents.azure.com" + "privatelink.azconfig.io" + "privatelink.azurecr.io" + "privatelink.cognitiveservices.azure.com" + "privatelink.azurecontainerapps.io" +) +for ZONE in "${DNS_ZONES[@]}"; do + az network private-dns zone create -g $AILZ_RG -n "$ZONE" -o none + LINK_NAME=$(echo "$ZONE" | sed 's/\./-/g')-link + az network private-dns link vnet create -g $AILZ_RG -z "$ZONE" -n "$LINK_NAME" -v "$VNET_ID" -e false -o none +done +echo " ✓ 6 Private DNS Zones creadas y vinculadas" + +# 4. Azure Bastion +echo "[4/6] Creando Azure Bastion..." +az network public-ip create -g $AILZ_RG -n "pip-bastion-demo" --sku Standard --allocation-method Static -l $LOCATION -o none +az network bastion create -g $AILZ_RG -n "bastion-demo" --public-ip-address "pip-bastion-demo" --vnet-name $VNET_NAME --sku Developer -l $LOCATION -o none +echo " ✓ Azure Bastion (Developer SKU) creado" + +# 5. JumpBox VM (Linux para costo mínimo) +echo "[5/6] Creando JumpBox VM..." +az vm create -g $AILZ_RG -n "jmp-ailz-demo" \ + --image "Ubuntu2404" --size "Standard_B2s" \ + --vnet-name $VNET_NAME --subnet "jumpbox-subnet" \ + --admin-username "azureuser" --generate-ssh-keys \ + --tags role=jumpbox purpose=mini-ailz \ + --public-ip-address "" -l $LOCATION -o none +echo " ✓ JumpBox Linux creada" + +# 6. Resumen +echo "" +echo "[6/6] Verificación..." +echo "" +echo "=== Mini-AILZ Creada Exitosamente ===" +echo "" +echo "Resource Group: $AILZ_RG" +echo "VNet: $VNET_NAME ($VNET_PREFIX)" +echo "Subredes:" +echo " - pe-subnet: $PE_SUBNET_PREFIX" +echo " - aca-env-subnet: $ACA_SUBNET_PREFIX" +echo " - jumpbox-subnet: $JUMPBOX_SUBNET_PREFIX" +echo " - AzureBastionSubnet: $BASTION_SUBNET_PREFIX" +echo "" +echo "Private DNS Zones: 6 zonas creadas y vinculadas" +echo "JumpBox: jmp-ailz-demo (acceso vía Bastion)" +echo "" +echo "=== Siguiente paso ===" +echo "Ejecuta el script de descubrimiento de ContentFlow:" +echo " cd infra/scripts && ./get-ailz-resources.sh --auto-set" +echo "Luego despliega:" +echo " azd env set DEPLOYMENT_MODE ailz-integrated" +echo " azd up" +``` + +--- + +## Apéndice B: Diagrama de Flujo de Despliegue + +``` +┌────────────────────────┐ +│ 1. Preparar Mini-AILZ │ +│ (create-mini-ailz.sh) │ +└────────┬───────────────┘ + │ + ▼ +┌────────────────────────┐ +│ 2. Obtener IDs │ +│ (get-ailz-resources │ +│ --auto-set) │ +└────────┬───────────────┘ + │ + ▼ +┌────────────────────────┐ +│ 3. Configurar azd │ +│ DEPLOYMENT_MODE= │ +│ ailz-integrated │ +└────────┬───────────────┘ + │ + ▼ +┌────────────────────────┐ +│ 4. azd up │ +│ (provision + deploy) │ +└────────┬───────────────┘ + │ + ▼ +┌────────────────────────┐ ┌──────────────────────┐ +│ 5. Conectar a │────▶│ 6. Probar servicios │ +│ JumpBox vía Bastion │ │ internos (API, Web) │ +└────────────────────────┘ └──────────────────────┘ +``` + +--- + +> **Última actualización:** Febrero 2026 +> **Aplica a:** ContentFlow con plantillas Bicep para despliegue `basic` y `ailz-integrated` diff --git a/Analysis/05-mini-ailz.md b/Analysis/05-mini-ailz.md new file mode 100644 index 0000000..f7e6714 --- /dev/null +++ b/Analysis/05-mini-ailz.md @@ -0,0 +1,1002 @@ +# Mini-AILZ con Terraform AVM Pattern Module para ContentFlow + +> **Documento técnico operativo** — Despliegue de una AI Landing Zone mínima usando el módulo oficial Terraform AVM `Azure/avm-ptn-aiml-landing-zone`, optimizada para el costo mínimo necesario para ContentFlow en modo `ailz-integrated`. + +--- + +## Tabla de Contenidos + +1. [Objetivo y Contexto](#1-objetivo-y-contexto) +2. [¿Por qué el Pattern Module en lugar de Azure CLI Manual?](#2-por-qué-el-pattern-module-en-lugar-de-azure-cli-manual) +3. [Análisis del Módulo: Todos los Recursos vs Lo Mínimo](#3-análisis-del-módulo-todos-los-recursos-vs-lo-mínimo) +4. [Mapeo: Requisitos ContentFlow → Parámetros del Módulo](#4-mapeo-requisitos-contentflow--parámetros-del-módulo) +5. [Configuración Terraform Mínima Completa](#5-configuración-terraform-mínima-completa) +6. [Conexión de Outputs con ContentFlow azd](#6-conexión-de-outputs-con-contentflow-azd) +7. [Fix Conocido: storage_use_azuread](#7-fix-conocido-storage_use_azuread) +8. [Paso a Paso: Despliegue](#8-paso-a-paso-despliegue) +9. [Comparación de Costo: Full vs Mini](#9-comparación-de-costo-full-vs-mini) +10. [Diagrama de Arquitectura Resultante](#10-diagrama-de-arquitectura-resultante) +11. [Notas de Operación y Troubleshooting](#11-notas-de-operación-y-troubleshooting) +12. [Limpieza de Recursos](#12-limpieza-de-recursos) + +--- + +## 1. Objetivo y Contexto + +### Situación + +ContentFlow soporta despliegue en modo `ailz-integrated`, el cual requiere infraestructura de red pre-existente (VNet, subredes, Private DNS Zones, Container Apps Environment). En el [documento 04](04-ai_lz_options.md) se describe cómo crear esta infraestructura con comandos Azure CLI manuales. + +### Nuevo Enfoque + +En lugar de scripts manuales, usaremos el **módulo oficial Terraform AVM Pattern** para AI/ML Landing Zones: + +- **Módulo**: [`Azure/avm-ptn-aiml-landing-zone/azurerm`](https://github.com/Azure/terraform-azurerm-avm-ptn-aiml-landing-zone) +- **Versión**: `0.4.0` +- **Registry**: [Terraform Registry](https://registry.terraform.io/modules/Azure/avm-ptn-aiml-landing-zone/azurerm/latest) + +### Principio Clave: Solo lo Mínimo Indispensable + +El módulo completo despliega **más de 25 recursos** incluyendo AI Foundry, App Gateway, Firewall, APIM, Build VM, AI Search, Bing Grounding, etc. ContentFlow **no necesita la mayoría de estos**. Este documento configura **exclusivamente** lo que ContentFlow requiere para funcionar en modo privado. + +--- + +## 2. ¿Por qué el Pattern Module en lugar de Azure CLI Manual? + +| Aspecto | Azure CLI Manual (Doc 04) | Terraform AVM Module (Este Doc) | +|---|---|---| +| **Reproducibilidad** | Scripts bash con variables | Declarativo, idempotente, plan/apply | +| **Estado** | No hay tracking de estado | `terraform.tfstate` con full tracking | +| **Drift detection** | Manual (`az resource show`) | `terraform plan` detecta drift | +| **Limpieza** | `az group delete` (todo o nada) | `terraform destroy` selectivo | +| **Validación** | Post-ejecución manual | `terraform validate` + `plan` pre-apply | +| **Subredes y DNS** | Creación manual una por una | El módulo crea subredes automáticamente | +| **Private DNS Zones** | 6 comandos individuales + links | El módulo las crea y vincula | +| **Container Apps Env** | No incluido en doc 04 | Incluido con VNet integration | +| **Bastion** | Configuración manual compleja | Un flag `deploy = true` | +| **Versionamiento** | Copiar/pegar scripts | Module version pinning | +| **Soporte oficial** | Ninguno | Módulo oficial de Microsoft (AVM) | + +**Ventaja principal**: El módulo maneja internamente la creación de subredes con los nombres, tamaños y delegaciones correctas. Esto elimina errores comunes como subredes con prefijos incorrectos o sin delegación para Container Apps. + +--- + +## 3. Análisis del Módulo: Todos los Recursos vs Lo Mínimo + +### Recursos que el módulo PUEDE desplegar (despliegue completo) + +| Recurso | Default `deploy` | ¿ContentFlow lo necesita? | Acción Mini-AILZ | +|---|---|---|---| +| **VNet + Subredes** | Siempre (required) | **SÍ** | ✅ Mantener | +| **Private DNS Zones** | Siempre | **SÍ** | ✅ Mantener | +| **Log Analytics Workspace** | `true` | **SÍ** (compartido) | ✅ Mantener | +| **NSGs** | Siempre | **SÍ** (seguridad) | ✅ Mantener | +| **Container Apps Environment** | `true` | **SÍ** | ✅ Mantener | +| **Bastion** | `true` | **SÍ** (acceso a red privada) | ✅ Mantener | +| **GenAI Container Registry** | `true` | **Parcial** (ContentFlow crea el suyo) | ⚠️ Mantener* | +| **GenAI Key Vault** | Siempre | **Parcial** | ⚠️ Mantener* | +| **GenAI Storage Account** | `true` | **No directamente** | ⚠️ Mantener* | +| **GenAI App Configuration** | `true` | **No directamente** | ⚠️ Mantener* | +| **GenAI Cosmos DB** | `true` | **No directamente** | ⚠️ Mantener* | +| **AI Foundry Hub + BYOR** | `{}` (vacío = no crea) | **No** | ❌ Omitir | +| **AI Model Deployments** | `{}` (vacío = no crea) | **No** | ❌ Omitir | +| **AI Projects** | `{}` (vacío = no crea) | **No** | ❌ Omitir | +| **App Gateway** | `null` (no crea) | **No** | ❌ Omitir | +| **Azure Firewall** | `true` | **No** (demo) | ❌ Deshabilitar | +| **Build VM / Jump VM** | `true` | **No** (usamos Bastion) | ❌ Deshabilitar | +| **AI Search (BYOR)** | `{}` (vacío = no crea) | **No** | ❌ Omitir | +| **AI Search (KS)** | `true` | **No** | ❌ Deshabilitar | +| **Bing Grounding** | `true` | **No** | ❌ Deshabilitar | +| **APIM** | `true` | **No** | ❌ Deshabilitar | +| **WAF Policy** | Solo con App GW | **No** | ❌ Omitir | + +> **\*** Los recursos GenAI (Container Registry, Key Vault, Storage, App Config, Cosmos) se crean por defecto como parte de la plataforma GenAI del módulo. ContentFlow crea sus propios recursos de datos (Cosmos, Storage, App Config) durante `azd up`, pero el módulo necesita el Container Registry y Key Vault internamente. Para minimizar costo, podemos aceptar estos con configuración por defecto ligera. + +### ¿Qué NO se puede deshabilitar? + +El módulo **siempre crea** estos recursos independientemente de la configuración: + +1. **Resource Group** (si no existe) +2. **VNet** (required input) +3. **Subredes** (calculadas internamente según recursos habilitados) +4. **Private DNS Zones** (necesarias para private endpoints) +5. **NSGs** (asociados a subredes) +6. **Key Vault** (usado internamente por el módulo para secretos de GenAI) +7. **Role Assignments** (deployment user → Key Vault Admin) + +--- + +## 4. Mapeo: Requisitos ContentFlow → Parámetros del Módulo + +### Lo que ContentFlow Necesita del AILZ + +| Requisito ContentFlow | Parámetro del Módulo | Variable de Entorno `azd` | +|---|---|---| +| VNet Resource ID | `module.ailz.virtual_network.id` | `EXISTING_VNET_RESOURCE_ID` | +| Subred `pe-subnet` | Creada automáticamente por el módulo | `PRIVATE_ENDPOINT_SUBNET_NAME` | +| Subred `aca-env-subnet` | Creada automáticamente por el módulo | `CONTAINER_APPS_SUBNET_NAME` | +| DNS: `privatelink.blob.core.windows.net` | Parte de `private_dns_zones` del módulo | `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.documents.azure.com` | Parte de `private_dns_zones` del módulo | `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.azconfig.io` | Parte de `private_dns_zones` del módulo | `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.azurecr.io` | Parte de `private_dns_zones` del módulo | `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.cognitiveservices.azure.com` | Parte de `private_dns_zones` del módulo | `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.azurecontainerapps.io` | Parte de `private_dns_zones` del módulo | `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | +| Log Analytics Workspace | `module.ailz.log_analytics_workspace_id` | `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | +| Container Apps Environment | Creado por el módulo (VNet-integrated, interno) | El CAE se crea aquí, ContentFlow lo usa | + +### Nota Importante sobre Subredes + +El módulo AVM Pattern calcula y crea las subredes internamente basándose en qué recursos están habilitados. Los nombres de subredes son determinados por el módulo. ContentFlow espera subredes llamadas `pe-subnet` y `aca-env-subnet`. Puedes usar el bloque `vnet_definition.subnets` para configurar override de nombres si es necesario. + +### Nota sobre Container Apps Environment + +El módulo crea un Container Apps Environment integrado con la VNet. Esto es **exactamente lo que ContentFlow necesita**. Sin embargo, ContentFlow normalmente crea su propio CAE durante `azd up`. Para el modo Mini-AILZ con Terraform, tienes dos opciones: + +1. **Usar el CAE del módulo** — Más eficiente; ContentFlow debe configurarse para no crear un CAE propio +2. **Deshabilitar el CAE del módulo** — ContentFlow crea el suyo durante deploy, usando la subred apropiada + +La **opción recomendada es la 1** (usar el CAE del módulo) ya que garantiza la configuración correcta de VNet integration. + +--- + +## 5. Configuración Terraform Mínima Completa + +### Estructura de Archivos + +``` +mini-ailz-contentflow/ +├── terraform.tf # Providers y versiones +├── variables.tf # Variables de entrada +├── main.tf # Data sources y helpers +├── ailz.tf # Módulo AILZ con configuración mínima +├── outputs.tf # Outputs para ContentFlow +└── terraform.tfvars # Valores (opcional, gitignored) +``` + +### `terraform.tf` — Providers + +```hcl +terraform { + required_version = ">= 1.9, < 2.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.116, < 5.0" + } + azapi = { + source = "Azure/azapi" + version = "~> 2.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.5" + } + } +} + +provider "azurerm" { + features { + cognitive_account { + purge_soft_delete_on_destroy = true + } + key_vault { + purge_soft_delete_on_destroy = true + } + resource_group { + prevent_deletion_if_contains_resources = false + } + } + + # CRÍTICO: Sin esto, las Storage Accounts dan error 403 + # Ver sección 7 para detalles + storage_use_azuread = true +} + +provider "azapi" {} +``` + +### `variables.tf` — Variables de Entrada + +```hcl +variable "location" { + type = string + description = "Región de Azure para los recursos" + default = "swedencentral" +} + +variable "resource_group_name" { + type = string + description = "Nombre del Resource Group para la Mini-AILZ" + default = "rg-mini-ailz-contentflow" +} + +variable "name_prefix" { + type = string + description = "Prefijo para nombres de recursos (máx 10 chars, minúsculas alfanuméricas)" + default = "cfailz" + + validation { + condition = length(var.name_prefix) <= 10 && can(regex("^[a-z0-9]+$", var.name_prefix)) + error_message = "name_prefix debe tener máximo 10 caracteres alfanuméricos en minúsculas." + } +} + +variable "enable_telemetry" { + type = bool + description = "Habilitar telemetría del módulo AVM" + default = false +} +``` + +### `main.tf` — Data Sources + +```hcl +data "azurerm_client_config" "current" {} +``` + +### `ailz.tf` — Módulo AILZ Configuración Mínima + +```hcl +# ============================================================================= +# Mini-AILZ para ContentFlow +# ============================================================================= +# Este archivo configura SOLO lo mínimo indispensable del módulo AVM Pattern +# para satisfacer los requisitos de ContentFlow en modo ailz-integrated: +# 1. VNet con subredes (pe-subnet, aca-env-subnet) +# 2. Private DNS Zones vinculadas a la VNet +# 3. Container Apps Environment (VNet-integrated, internal LB) +# 4. Bastion (acceso a la red privada) +# 5. Log Analytics Workspace (monitoreo compartido) +# 6. NSGs (seguridad de subredes) +# 7. Key Vault + Container Registry (requeridos internamente por el módulo) +# +# TODO lo demás está DESHABILITADO para minimizar costo: +# ✗ AI Foundry, Model Deployments, AI Projects +# ✗ App Gateway, WAF Policy +# ✗ Azure Firewall +# ✗ Build VM, Jump VM +# ✗ AI Search (BYOR y KS) +# ✗ Bing Grounding +# ✗ APIM +# ============================================================================= + +module "ailz" { + source = "Azure/avm-ptn-aiml-landing-zone/azurerm" + version = "0.4.0" + + # ── Ubicación y Resource Group ────────────────────────────────────────────── + location = var.location + resource_group_name = var.resource_group_name + + # ── VNet (REQUIRED) ──────────────────────────────────────────────────────── + # ContentFlow necesita: + # - pe-subnet: para Private Endpoints (~32 IPs, /27+) + # - aca-env-subnet: para Container Apps Environment (~512 IPs, /23) + # + # NOTA: El módulo calcula y crea las subredes internamente. + # Usamos 10.0.0.0/16 para tener espacio suficiente. + vnet_definition = { + address_space = ["10.0.0.0/16"] + enable_diagnostic_settings = false + } + + # ── Container Apps Environment ───────────────────────────────────────────── + # ContentFlow despliega API, Worker y Web como Container Apps. + # Este CAE se integra con la VNet en modo interno (solo accesible desde VNet). + container_app_environment_definition = { + internal_load_balancer_enabled = true + zone_redundancy_enabled = false # false = ahorro de costo para demo + enable_diagnostic_settings = false + workload_profile = [ + { + name = "Consumption" + workload_profile_type = "Consumption" + } + ] + } + + # ── Bastion ──────────────────────────────────────────────────────────────── + # Necesario para acceder a la red privada (UI de ContentFlow, debug, etc.) + # SKU "Basic" es más barato que "Standard" (~$140/mes vs ~$350/mes) + bastion_definition = { + deploy = true + sku = "Basic" + zones = [] # Sin zone redundancy para demo + } + + # ── Log Analytics Workspace ──────────────────────────────────────────────── + # Compartido con ContentFlow para monitoreo centralizado. + law_definition = { + deploy = true + } + + # ── AI Foundry ───────────────────────────────────────────────────────────── + # ContentFlow NO necesita AI Foundry del AILZ. + # (ContentFlow crea su propio AI Foundry durante azd up) + # Dejamos el default vacío {} = no crea hub, projects, ni BYOR resources. + ai_foundry_definition = { + create_byor = false # No crear BYOR resources (AI Search, Cosmos, KV, Storage) + ai_foundry = { + enable_diagnostic_settings = false + } + } + + # ── DESHABILITADOS ───────────────────────────────────────────────────────── + + # App Gateway: ContentFlow no usa App Gateway (acceso via Bastion/VPN) + # Default es null = no se despliega + app_gateway_definition = null + + # Azure Firewall: No necesario para demo (ahorra ~$250/mes) + firewall_definition = { + deploy = false + } + + # Build VM: No necesaria (usamos Bastion + local development) + buildvm_definition = { + deploy = false + } + + # Jump VM: No necesaria si tenemos Bastion + # (jumpvm_definition no tiene deploy flag, se controla por buildvm) + + # KS AI Search: ContentFlow no necesita AI Search federado + ks_ai_search_definition = { + deploy = false + } + + # Bing Grounding: ContentFlow no usa Bing + ks_bing_grounding_definition = { + deploy = false + } + + # APIM: ContentFlow no usa API Management + apim_definition = { + deploy = false + publisher_email = "noreply@example.com" + publisher_name = "N/A" + } + + # ── GenAI Platform Resources ─────────────────────────────────────────────── + # Estos recursos se crean por defecto como parte de la plataforma GenAI. + # ContentFlow crea sus propios Cosmos DB, Storage, App Config durante azd up, + # pero el módulo internamente necesita algunos de estos. + # Configuración mínima para reducir costo: + + genai_container_registry_definition = { + zone_redundancy_enabled = false # Ahorro: sin ZRS + enable_diagnostic_settings = false + } + + genai_key_vault_definition = { + # Acceso público habilitado para facilitar deploy desde local + # En producción: false + Private Endpoint + public_network_access_enabled = true + network_acls = { + bypass = "AzureServices" + default_action = "Deny" + } + } + + genai_storage_account_definition = { + account_replication_type = "LRS" # LRS es más barato que GRS (default) + enable_diagnostic_settings = false + } + + genai_app_configuration_definition = { + purge_protection_enabled = false # Facilita destroy para demo + enable_diagnostic_settings = false + } + + genai_cosmosdb_definition = { + analytical_storage_enabled = false # No necesario para demo + automatic_failover_enabled = false # No necesario para single-region demo + enable_diagnostic_settings = false + } + + # ── Flags y otros ────────────────────────────────────────────────────────── + # flag_platform_landing_zone = false → No crea route tables con Firewall + # (Si es true, se asume que hay un Firewall y crea UDRs para enrutar tráfico) + flag_platform_landing_zone = false + + enable_telemetry = var.enable_telemetry + name_prefix = var.name_prefix +} +``` + +### `outputs.tf` — Valores para ContentFlow + +```hcl +# ============================================================================= +# Outputs para configurar ContentFlow azd environment +# ============================================================================= +# Estos valores se usan como variables de entorno para: +# azd env set +# antes de ejecutar: azd up --deployment-mode ailz-integrated +# ============================================================================= + +output "vnet_resource_id" { + description = "VNet Resource ID → EXISTING_VNET_RESOURCE_ID" + value = module.ailz.virtual_network.id +} + +output "log_analytics_workspace_id" { + description = "Log Analytics Workspace ID → EXISTING_LOG_ANALYTICS_WORKSPACE_ID" + value = module.ailz.log_analytics_workspace_id +} + +output "subnets" { + description = "Mapa de subredes creadas (para verificar nombres)" + value = module.ailz.subnets +} + +# ============================================================================= +# NOTA SOBRE PRIVATE DNS ZONES: +# ============================================================================= +# El módulo crea las Private DNS Zones automáticamente, pero actualmente (v0.4.0) +# NO expone sus Resource IDs como outputs directos. +# +# Para obtener los IDs de las DNS Zones, después de `terraform apply`: +# +# az network private-dns zone list \ +# --resource-group \ +# --query "[].{name:name, id:id}" -o table +# +# Alternativamente, puedes usar data sources de Terraform para leerlos: +# ============================================================================= + +# Instrucciones que se imprimen después del apply +output "contentflow_setup_instructions" { + description = "Instrucciones para configurar ContentFlow con esta Mini-AILZ" + value = <<-EOT + + ╔══════════════════════════════════════════════════════════════════╗ + ║ Mini-AILZ desplegada exitosamente para ContentFlow ║ + ╚══════════════════════════════════════════════════════════════════╝ + + Próximos pasos: + + 1. Obtener los Resource IDs de las Private DNS Zones: + + az network private-dns zone list \ + --resource-group ${var.resource_group_name} \ + --query "[].{name:name, id:id}" -o table + + 2. Configurar el environment de ContentFlow: + + cd + azd env set DEPLOYMENT_MODE ailz-integrated + azd env set EXISTING_VNET_RESOURCE_ID ${module.ailz.virtual_network.id} + azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID ${module.ailz.log_analytics_workspace_id} + + # Copiar los IDs de DNS Zones del paso 1: + azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID + + 3. Desplegar ContentFlow: + + azd up + + EOT +} +``` + +--- + +## 6. Conexión de Outputs con ContentFlow azd + +Después de ejecutar `terraform apply`, necesitas pasar los valores de infraestructura a ContentFlow. + +### Script Automatizado: `configure-contentflow.sh` + +```bash +#!/bin/bash +# ============================================================================= +# configure-contentflow.sh +# Configura las variables de entorno de ContentFlow azd con los outputs +# de la Mini-AILZ desplegada con Terraform. +# ============================================================================= +set -euo pipefail + +# Verificar que estamos en el directorio correcto de Terraform +if [[ ! -f "ailz.tf" ]]; then + echo "ERROR: Ejecuta este script desde el directorio de Terraform Mini-AILZ" + exit 1 +fi + +# Directorio de ContentFlow (ajustar según tu setup) +CONTENTFLOW_DIR="${1:-../contentflow-test-001}" + +echo "Obteniendo outputs de Terraform..." +VNET_ID=$(terraform output -raw vnet_resource_id) +LAW_ID=$(terraform output -raw log_analytics_workspace_id) +RG_NAME=$(terraform output -raw 2>/dev/null | grep -oP 'resource-group \K[^ \\]+' || true) + +# Obtener el nombre real del resource group del state +RG_NAME=$(terraform show -json | python3 -c " +import sys, json +state = json.load(sys.stdin) +for r in state.get('values', {}).get('root_module', {}).get('child_modules', []): + for res in r.get('resources', []): + if res['type'] == 'azurerm_resource_group': + print(res['values']['name']) + break +" 2>/dev/null || echo "rg-mini-ailz-contentflow") + +echo "Resource Group: $RG_NAME" +echo "VNet ID: $VNET_ID" +echo "Log Analytics ID: $LAW_ID" + +echo "" +echo "Obteniendo Private DNS Zone IDs..." + +# Función helper para obtener DNS Zone ID +get_dns_zone_id() { + local zone_name=$1 + az network private-dns zone show \ + --resource-group "$RG_NAME" \ + --name "$zone_name" \ + --query id -o tsv 2>/dev/null || echo "" +} + +BLOB_DNS=$(get_dns_zone_id "privatelink.blob.core.windows.net") +COSMOS_DNS=$(get_dns_zone_id "privatelink.documents.azure.com") +APPCONFIG_DNS=$(get_dns_zone_id "privatelink.azconfig.io") +ACR_DNS=$(get_dns_zone_id "privatelink.azurecr.io") +COGNITIVE_DNS=$(get_dns_zone_id "privatelink.cognitiveservices.azure.com") +CONTAINERAPP_DNS=$(get_dns_zone_id "privatelink.azurecontainerapps.io") + +# Verificar que encontramos todas las zonas +MISSING=0 +for ZONE_VAR in BLOB_DNS COSMOS_DNS APPCONFIG_DNS ACR_DNS COGNITIVE_DNS CONTAINERAPP_DNS; do + if [[ -z "${!ZONE_VAR}" ]]; then + echo "⚠ WARNING: No se encontró DNS Zone para $ZONE_VAR" + MISSING=1 + fi +done + +if [[ $MISSING -eq 1 ]]; then + echo "" + echo "Algunas DNS Zones no se encontraron. Verifica con:" + echo " az network private-dns zone list --resource-group $RG_NAME -o table" + echo "" + echo "El módulo puede usar nombres diferentes. Ajusta manualmente si es necesario." +fi + +echo "" +echo "Configurando azd environment en: $CONTENTFLOW_DIR" + +pushd "$CONTENTFLOW_DIR" > /dev/null + +azd env set DEPLOYMENT_MODE ailz-integrated +azd env set EXISTING_VNET_RESOURCE_ID "$VNET_ID" +azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$LAW_ID" +[[ -n "$BLOB_DNS" ]] && azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID "$BLOB_DNS" +[[ -n "$COSMOS_DNS" ]] && azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID "$COSMOS_DNS" +[[ -n "$APPCONFIG_DNS" ]] && azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID "$APPCONFIG_DNS" +[[ -n "$ACR_DNS" ]] && azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID "$ACR_DNS" +[[ -n "$COGNITIVE_DNS" ]] && azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID "$COGNITIVE_DNS" +[[ -n "$CONTAINERAPP_DNS" ]] && azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$CONTAINERAPP_DNS" + +popd > /dev/null + +echo "" +echo "✅ Configuración completada. Ejecuta:" +echo " cd $CONTENTFLOW_DIR && azd up" +``` + +### Mapeo Completo de Outputs → Variables azd + +| Output Terraform / Comando az | Variable azd ContentFlow | Descripción | +|---|---|---| +| `module.ailz.virtual_network.id` | `EXISTING_VNET_RESOURCE_ID` | Resource ID de la VNet | +| `module.ailz.log_analytics_workspace_id` | `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | LAW compartido | +| `az ... privatelink.blob.core.windows.net` | `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | DNS Zone para Blob | +| `az ... privatelink.documents.azure.com` | `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | DNS Zone para Cosmos | +| `az ... privatelink.azconfig.io` | `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | DNS Zone para App Config | +| `az ... privatelink.azurecr.io` | `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | DNS Zone para ACR | +| `az ... privatelink.cognitiveservices.azure.com` | `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | DNS Zone para AI Services | +| `az ... privatelink.azurecontainerapps.io` | `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | DNS Zone para Container Apps | +| (contenido en `pe-subnet`) | `PRIVATE_ENDPOINT_SUBNET_NAME` | Default: `pe-subnet` | +| (contenido en `aca-env-subnet`) | `CONTAINER_APPS_SUBNET_NAME` | Default: `aca-env-subnet` | + +--- + +## 7. Fix Conocido: storage_use_azuread + +### El Problema + +El módulo crea Storage Accounts con `shared_access_key_enabled = false` (default seguro). Sin embargo, el provider `azurerm` por defecto usa **autenticación por key** para operaciones de Storage. Esto produce: + +``` +Error: StatusCode=403 -- Original Error: autorest/azure: Service returned an error. +Status=403 Code="AuthorizationFailure" +``` + +### La Solución + +Agregar `storage_use_azuread = true` en el bloque del provider: + +```hcl +provider "azurerm" { + features { ... } + storage_use_azuread = true # ← CRÍTICO +} +``` + +### Prerrequisito + +El usuario que ejecuta `terraform apply` debe tener el rol **Storage Blob Data Contributor** en la suscripción o en los Resource Groups correspondientes: + +```bash +# Asignar rol a tu usuario (una vez) +az role assignment create \ + --assignee "$(az ad signed-in-user show --query id -o tsv)" \ + --role "Storage Blob Data Contributor" \ + --scope "/subscriptions/$(az account show --query id -o tsv)" +``` + +--- + +## 8. Paso a Paso: Despliegue + +### 8.1 Preparar el Entorno + +```bash +# Crear directorio de trabajo +mkdir mini-ailz-contentflow && cd mini-ailz-contentflow + +# Crear los archivos Terraform (copiar de la sección 5) +# terraform.tf, variables.tf, main.tf, ailz.tf, outputs.tf +``` + +### 8.2 Inicializar Terraform + +```bash +terraform init +``` + +Esto descarga el módulo AVM y todos sus sub-módulos (~20 módulos). + +### 8.3 Validar y Planificar + +```bash +# Validar sintaxis +terraform validate + +# Ver plan de ejecución +terraform plan -out=tfplan +``` + +El plan debería mostrar aproximadamente **15-20 recursos** a crear (vs ~45+ con el módulo completo). + +### 8.4 Aplicar + +```bash +terraform apply tfplan +``` + +Tiempo estimado: **15-25 minutos** (Bastion y Container Apps Environment son los más lentos). + +### 8.5 Verificar Recursos Creados + +```bash +# Listar recursos en el RG +az resource list \ + --resource-group rg-mini-ailz-contentflow \ + --query "[].{name:name, type:type}" \ + -o table +``` + +Deberías ver: +- Virtual Network +- Subredes (varias, creadas por el módulo) +- NSGs +- Private DNS Zones (6+) +- Bastion Host + Public IP +- Container Apps Environment +- Log Analytics Workspace +- Key Vault +- Container Registry +- Storage Account +- App Configuration +- Cosmos DB Account + +### 8.6 Configurar ContentFlow + +```bash +# Opción A: Script automatizado +chmod +x configure-contentflow.sh +./configure-contentflow.sh /path/to/contentflow + +# Opción B: Manual +cd /path/to/contentflow +azd env set DEPLOYMENT_MODE ailz-integrated +azd env set EXISTING_VNET_RESOURCE_ID "$(terraform -chdir=/path/to/mini-ailz output -raw vnet_resource_id)" +# ... (ver sección 6 para el mapeo completo) +``` + +### 8.7 Desplegar ContentFlow + +```bash +cd /path/to/contentflow +azd up +``` + +--- + +## 9. Comparación de Costo: Full vs Mini + +### Despliegue Completo del Módulo (tu test anterior en standalone-test001) + +| Recurso | SKU/Tier | Costo Estimado/mes | +|---|---|---| +| AI Foundry Hub + BYOR | S0 | ~$0 (pago por uso) | +| GPT-4.1 Model | GlobalStandard x1 | ~$0-30 (por tokens) | +| AI Search | Standard, 2 replicas | **~$500** | +| App Gateway (WAF_v2) | 2 scale units | **~$350** | +| Azure Bastion | Standard, 3 AZs | **~$350** | +| Azure Firewall | Standard, 3 AZs | **~$250** | +| Build VM | Standard_B2s | **~$30** | +| APIM | Premium x3 | **~$2,100** | +| Container Apps Env | Consumption (ZR) | ~$0-20 | +| Cosmos DB (BYOR) | Serverless | ~$0-10 | +| Cosmos DB (GenAI) | Serverless | ~$0-10 | +| Key Vault x2 | Standard | ~$0-5 | +| Storage Account x2 | ZRS/GRS | ~$5-15 | +| Container Registry | Premium (ZR) | **~$60** | +| App Configuration | Standard | ~$36 | +| Log Analytics | Per-GB | ~$0-20 | +| Bing Grounding | G1 | ~$5 | +| KS AI Search | Standard | **~$250** | +| VNet + Subredes | - | ~$0 | +| Private DNS Zones | - | ~$1-5 | +| **TOTAL ESTIMADO** | | **~$3,500-4,000/mes** | + +### Mini-AILZ (este documento) + +| Recurso | SKU/Tier | Costo Estimado/mes | +|---|---|---| +| Azure Bastion | **Basic**, sin AZs | **~$140** | +| Container Apps Env | Consumption, sin ZR | ~$0-20 | +| Container Registry | Premium (requerido para PE) | **~$50** | +| Cosmos DB (GenAI) | Serverless | ~$0-10 | +| Key Vault | Standard | ~$0-5 | +| Storage Account | **LRS** | ~$2-5 | +| App Configuration | Standard | ~$36 | +| Log Analytics | Per-GB | ~$0-20 | +| VNet + Subredes | - | ~$0 | +| Private DNS Zones | - | ~$1-5 | +| NSGs | - | ~$0 | +| **TOTAL ESTIMADO** | | **~$230-290/mes** | + +### Ahorro + +| Métrica | Full | Mini | Ahorro | +|---|---|---|---| +| Costo mensual | ~$3,700 | ~$260 | **~$3,440/mes (93%)** | +| Recursos Azure | ~40+ | ~15-20 | **50% menos** | +| Tiempo deploy | ~45 min | ~20 min | **55% menos** | + +> **Nota**: Los costos de ContentFlow en sí (Container Apps, Cosmos DB, Storage, etc. que crea `azd up`) son adicionales y similares en ambos escenarios (~$100-200/mes dependiendo del uso). + +--- + +## 10. Diagrama de Arquitectura Resultante + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Resource Group: rg-mini-ailz-contentflow │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ VNet (10.0.0.0/16) │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌──────────────────────────────────────┐ │ │ +│ │ │ Bastion Subnet │ │ Container Apps Subnet (/23) │ │ │ +│ │ │ ┌────────────┐ │ │ │ │ │ +│ │ │ │ Bastion │ │ │ ┌──────────────────────────────┐ │ │ │ +│ │ │ │ (Basic) │ │ │ │ Container Apps Environment │ │ │ │ +│ │ │ └────────────┘ │ │ │ (Internal Load Balancer) │ │ │ │ +│ │ └─────────────────┘ │ │ │ │ │ │ │ +│ │ │ │ │ ← ContentFlow despliega: │ │ │ │ +│ │ ┌─────────────────┐ │ │ │ API / Worker / Web │ │ │ │ +│ │ │ PE Subnet │ │ │ └──────────────────────────────┘ │ │ │ +│ │ │ │ │ └──────────────────────────────────────┘ │ │ +│ │ │ ← ContentFlow │ │ │ │ +│ │ │ crea PEs: │ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ ● Blob Storage │ │ │ Plataforma AILZ (módulo) │ │ │ +│ │ │ ● Cosmos DB │ │ │ │ │ │ +│ │ │ ● App Config │ │ │ ● Log Analytics Workspace │ │ │ +│ │ │ ● ACR │ │ │ ● Key Vault (GenAI) │ │ │ +│ │ │ ● AI Foundry │ │ │ ● Container Registry (GenAI) │ │ │ +│ │ └─────────────────┘ │ │ ● Storage Account (GenAI, LRS) │ │ │ +│ │ │ │ ● App Configuration (GenAI) │ │ │ +│ └───────────────────────│ │ ● Cosmos DB (GenAI) │ │ │ +│ │ │ ● NSGs │ │ │ +│ ┌────────────────────┐ │ └──────────────────────────────────────┘ │ │ +│ │ Private DNS Zones │ │ │ │ +│ │ (auto-vinculadas) │ └────────────────────────────────────────────┘ │ +│ │ │ │ +│ │ ● privatelink.blob.core.windows.net │ +│ │ ● privatelink.documents.azure.com │ +│ │ ● privatelink.azconfig.io │ +│ │ ● privatelink.azurecr.io │ +│ │ ● privatelink.cognitiveservices.azure.com │ +│ │ ● privatelink.azurecontainerapps.io │ +│ │ ● (+ otras del módulo: vault, openai, etc.) │ +│ └────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ Resource Group: rg-contentflow-ailz (creado por azd up) │ +│ │ +│ ● Storage Account (Blob + Queue) + Private Endpoints en pe-subnet │ +│ ● Cosmos DB + Private Endpoint en pe-subnet │ +│ ● App Configuration + Private Endpoint en pe-subnet │ +│ ● ACR Premium + Private Endpoint en pe-subnet │ +│ ● AI Foundry (Cognitive Services) + Private Endpoint en pe-subnet │ +│ ● Container Apps (API, Worker, Web) → Container Apps Environment │ +│ ● Application Insights → Log Analytics Workspace (compartido) │ +│ ● User Assigned Managed Identity + Role Assignments │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 11. Notas de Operación y Troubleshooting + +### 11.1 Nombres de Subredes + +El módulo AVM genera nombres de subredes internamente. ContentFlow espera `pe-subnet` y `aca-env-subnet`. Si los nombres generados por el módulo no coinciden, puedes: + +1. **Verificar nombres reales** después del apply: + ```bash + terraform output subnets + ``` + +2. **Configurar en ContentFlow** los nombres reales: + ```bash + azd env set PRIVATE_ENDPOINT_SUBNET_NAME "" + azd env set CONTAINER_APPS_SUBNET_NAME "" + ``` + +3. **Override en vnet_definition** (si el módulo lo permite): + ```hcl + vnet_definition = { + address_space = ["10.0.0.0/16"] + subnets = { + pe-subnet = { + name = "pe-subnet" + address_prefix = "10.0.1.0/27" + } + aca-env-subnet = { + name = "aca-env-subnet" + address_prefix = "10.0.16.0/23" + } + } + } + ``` + +### 11.2 Container Apps Environment Dual + +Si el módulo crea un Container Apps Environment pero ContentFlow también intenta crear uno durante `azd up`, podrías terminar con dos CAEs. Para evitar esto: + +- **Opción A**: Configurar ContentFlow para usar el CAE existente (pasar su ID como variable de entorno) +- **Opción B**: Deshabilitar el CAE en el módulo (`deploy = false` en `container_app_environment_definition`) y dejar que ContentFlow lo cree + +### 11.3 Error: "Address space conflict" + +Si ves errores sobre conflictos de rango de IP: + +``` +Error: creating Virtual Network: the address space "192.168.0.0/20" overlaps with... +``` + +Asegúrate de usar un rango que **no esté en uso** en tu suscripción. Recomendamos `10.0.0.0/16` en lugar de `192.168.0.0/x`. + +### 11.4 Error: "Insufficient subnet size" + +Container Apps Environment requiere una subred de al menos `/23` (512 IPs). El módulo normalmente calcula esto correctamente, pero si haces override de subredes, asegúrate de respetar este mínimo. + +### 11.5 Terraform Destroy: Orden de Dependencias + +Para destruir la Mini-AILZ: + +```bash +# PRIMERO: Destruir ContentFlow (tiene dependencias en la red) +cd /path/to/contentflow +azd down --force --purge + +# SEGUNDO: Destruir la Mini-AILZ +cd /path/to/mini-ailz +terraform destroy +``` + +> **Importante**: Si intentas destruir la Mini-AILZ mientras ContentFlow tiene Private Endpoints activos en las subredes, el destroy fallará. Siempre destruye ContentFlow primero. + +### 11.6 Restricción de Address Space para Foundry CapabilityHost + +Si alguna vez necesitas habilitar AI Foundry con agent service (`capabilityHost`), hay una restricción conocida: + +> El address space de la VNet **no puede ser** `192.168.0.0/16` (pero sí puede ser ranges dentro de él como `192.168.0.0/20`). Otros rangos RFC1918 como `10.0.0.0/8` y `172.16.0.0/12` **sí funcionan correctamente**. + +Nuestro rango `10.0.0.0/16` no tiene este problema. + +--- + +## 12. Limpieza de Recursos + +### Limpieza Completa (ContentFlow + Mini-AILZ) + +```bash +# 1. Destruir ContentFlow primero +cd /path/to/contentflow +azd down --force --purge + +# 2. Destruir Mini-AILZ +cd /path/to/mini-ailz +terraform destroy -auto-approve + +# 3. Verificar que el RG fue eliminado +az group show --name rg-mini-ailz-contentflow 2>/dev/null && echo "RG aún existe" || echo "RG eliminado" +``` + +### Limpieza Solo ContentFlow (mantener Mini-AILZ) + +```bash +# Solo destruir ContentFlow, la red permanece para reusar +cd /path/to/contentflow +azd down --force --purge +``` + +### Purge de Recursos con Soft-Delete + +Algunos recursos Azure tienen soft-delete. Si necesitas re-crear con el mismo nombre: + +```bash +# Key Vault +az keyvault purge --name + +# Cognitive Services +az cognitiveservices account purge \ + --location \ + --resource-group \ + --name + +# App Configuration +az appconfig purge --name +``` + +--- + +## Referencia Rápida + +### Versiones Utilizadas + +| Componente | Versión | +|---|---| +| Terraform | >= 1.9, < 2.0 | +| AVM Pattern Module | 0.4.0 | +| azurerm provider | >= 3.116, < 5.0 | +| azapi provider | ~> 2.0 | +| ContentFlow | Modo `ailz-integrated` | + +### Documentos Relacionados + +- [01-overview.md](01-overview.md) — Visión general de ContentFlow +- [02-architecture-detailed.md](02-architecture-detailed.md) — Arquitectura técnica detallada +- [03-use_cases_examples.md](03-use_cases_examples.md) — Casos de uso y ejemplos +- [04-ai_lz_options.md](04-ai_lz_options.md) — Guía de despliegue AILZ (Azure CLI manual) +- **[Módulo AVM en GitHub](https://github.com/Azure/terraform-azurerm-avm-ptn-aiml-landing-zone)** — Código fuente y documentación completa +- **[Módulo AVM en Terraform Registry](https://registry.terraform.io/modules/Azure/avm-ptn-aiml-landing-zone/azurerm/latest)** — Registry oficial + +--- + +> **Última actualización**: Julio 2025 +> **Módulo fuente**: `Azure/avm-ptn-aiml-landing-zone/azurerm` v0.4.0 +> **Autor**: Análisis técnico para demos ContentFlow con suscripción única diff --git a/personal-private-configurations.md b/personal-private-configurations.md new file mode 100644 index 0000000..4ec4a38 --- /dev/null +++ b/personal-private-configurations.md @@ -0,0 +1,141 @@ +# Personal Private Configurations + +> **WARNING**: This file contains environment-specific configurations. Do NOT commit to a shared/public repository. + +--- + +## Variables + +Update these values to reuse all scripts below in any environment. + +```bash +# ── Environment ── +RESOURCE_GROUP="rg-contentflow-test-001" +SUBSCRIPTION_ID="d12d19a9-0636-4951-90a4-339158fd57d8" + +# ── Container App names ── +APP_API="api-vtkhgrhgdl4w2" +APP_WEB="web-vtkhgrhgdl4w2" +APP_WORKER="worker-vtkhgrhgdl4w2" +APPS=("$APP_API" "$APP_WEB" "$APP_WORKER") + +# ── Networking ── +MY_IP=$(curl -s ifconfig.me) +RULE_NAME="allow-my-ip" + +# ── Endpoints ── +API_URL="https://api-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" +WEB_URL="https://web-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" +WORKER_URL="https://worker-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" +``` + +--- + +## IP Access Restrictions on Azure Container Apps + +**Date Applied**: March 16, 2026 +**Resource Group**: `rg-contentflow-test-001` +**Subscription**: `ME-MngEnvMCAP887462-gereyeso-1` (`d12d19a9-0636-4951-90a4-339158fd57d8`) +**Region**: East US 2 + +### What was configured + +All three Container Apps were restricted to accept external ingress traffic **only** from a single IP address. Any request from a different IP receives a `403 Forbidden` response. + +| Container App | Allowed IP | Rule Name | +|---------------|-----------|-----------| +| `api-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | +| `web-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | +| `worker-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | + +### Service Endpoints (restricted) + +| Service | URL | +|---------|-----| +| Web UI | `$WEB_URL` | +| API | `$API_URL` | +| API Docs | `$API_URL/docs` | +| Worker | `$WORKER_URL` | + +### Why this is safe for internal communication + +Container Apps within the **same Container Apps Environment** communicate through the internal network. IP access restrictions apply only to **external** ingress traffic (from the internet). The service-to-service flow remains unaffected: + +``` +Browser (your IP) → Web UI → (browser fetches from) → API → (internal) → Worker +``` + +- **Web → API**: The React SPA runs in your browser, so API calls originate from your IP. +- **API → Worker**: Both are in the same Container Apps Environment; internal traffic bypasses IP restrictions. + +--- + +### Apply IP restriction to all apps + +```bash +for app in "${APPS[@]}"; do + echo "Restricting $app to $MY_IP ..." + az containerapp ingress access-restriction set \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --rule-name "$RULE_NAME" \ + --ip-address "$MY_IP/32" \ + --action Allow \ + --description "Allow only my IP" +done +``` + +### Update IP on all apps (when your IP changes) + +```bash +MY_IP=$(curl -s ifconfig.me) +echo "New IP: $MY_IP" + +for app in "${APPS[@]}"; do + echo "Updating $app ..." + az containerapp ingress access-restriction remove \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --rule-name "$RULE_NAME" + + az containerapp ingress access-restriction set \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --rule-name "$RULE_NAME" \ + --ip-address "$MY_IP/32" \ + --action Allow \ + --description "Allow only my IP" +done +``` + +### Remove all restrictions (make public again) + +```bash +for app in "${APPS[@]}"; do + echo "Removing restriction from $app ..." + az containerapp ingress access-restriction remove \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --rule-name "$RULE_NAME" +done +``` + +### Verify current restrictions + +```bash +for app in "${APPS[@]}"; do + echo "=== $app ===" + az containerapp ingress access-restriction list \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --output table +done +``` + +### Quick health check + +```bash +for url in "$API_URL/health" "$WEB_URL" "$WORKER_URL"; do + echo "$url → $(curl -s -o /dev/null -w '%{http_code}' "$url")" +done +``` From 08f2f936620102234f8fb5d33fd22d57fc673de3 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Thu, 19 Mar 2026 16:24:45 -0400 Subject: [PATCH 02/29] issues --- changes.md | 161 +++++++++++++++++++++++++++++++++++++++++ infra/bicep/main.bicep | 4 +- 2 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 changes.md diff --git a/changes.md b/changes.md new file mode 100644 index 0000000..51e6816 --- /dev/null +++ b/changes.md @@ -0,0 +1,161 @@ +# Deployment Fixes – `azd up` Template Validation Errors + +## Table of Contents + +- [1. Fix: LogAnalytics CustomerId Must Be a GUID](#1-fix-loganalytics-customerid-must-be-a-guid) +- [2. Fix: Property Name Typo `privateEndpointsSubnetId`](#2-fix-property-name-typo-privateendpointssubnetid) +- [3. Fix: `UnmatchedPrincipalType` – `deployer().objectId` Hardcoded as `User`](#3-fix-unmatchedprincipaltype--deployerobjectid-hardcoded-as-user) + +--- + +## 1. Fix: LogAnalytics CustomerId Must Be a GUID + +**Error:** + +``` +InvalidRequestParameterWithDetails: LogAnalyticsConfiguration.CustomerId is invalid. +CustomerId must be a GUID without additional whiteSpace. +``` + +**Root Cause:** + +In `infra/bicep/main.bicep`, the `containerAppsEnvironment` module was receiving the full ARM **resource ID** of the existing Log Analytics workspace (e.g., `/subscriptions/.../providers/Microsoft.OperationalInsights/workspaces/ailz-law`) as the `logAnalyticsWorkspaceId` parameter. However, the Container Apps Environment module uses this value as the `customerId` in `logAnalyticsConfiguration`, which requires the **Workspace GUID** — not the ARM resource path. + +This only affects AILZ-integrated deployments that reference a pre-existing Log Analytics workspace via the `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` environment variable. + +**File:** `infra/bicep/main.bicep` (containerAppsEnvironment module call) + +**Before:** + +```bicep +logAnalyticsWorkspaceId: !empty(existingLogAnalyticsWorkspaceId) + ? existingLogAnalyticsWorkspaceId + : logAnalytics!.outputs.logAnalyticsWorkspaceId +``` + +**After:** + +```bicep +logAnalyticsWorkspaceId: !empty(existingLogAnalyticsWorkspaceId) + ? reference(existingLogAnalyticsWorkspaceId, '2021-12-01-preview').customerId + : logAnalytics!.outputs.logAnalyticsWorkspaceId +``` + +The `reference()` function retrieves the workspace properties at deploy time and extracts the `customerId` (GUID), which is the format the Container Apps Environment requires. + +--- + +## 2. Fix: Property Name Typo `privateEndpointsSubnetId` + +**Error:** + +``` +InvalidTemplate: The language expression property 'privateEndpointsSubnetId' doesn't exist, +available properties are 'enablePrivateEndpoints, publicNetworkAccess, vnetResourceId, +privateEndpointSubnetId, containerAppsSubnetId, privateDnsZoneIds'. +``` + +**Root Cause:** + +In `infra/bicep/main.bicep`, the `containerRegistry` module call referenced `networkConfig.privateEndpointsSubnetId` (plural, with an extra **s**), but the `networkConfig` variable defines the property as `privateEndpointSubnetId` (singular). + +**File:** `infra/bicep/main.bicep` (containerRegistry module call) + +**Before:** + +```bicep +privateEndpointSubnetId: isAILZIntegrated ? networkConfig.privateEndpointsSubnetId : '' +``` + +**After:** + +```bicep +privateEndpointSubnetId: isAILZIntegrated ? networkConfig.privateEndpointSubnetId : '' +``` + +Simple typo fix — `privateEndpointsSubnetId` → `privateEndpointSubnetId`. + +--- + +## 3. Fix: `UnmatchedPrincipalType` – `deployer().objectId` Hardcoded as `User` + +**Error:** + +``` +UnmatchedPrincipalType: The PrincipalId '634c8d99dc1442e18389ece9c4fe7e8a' has type +'ServicePrincipal', which is different from specified PrincipalType 'User'. +``` + +**Root Cause:** + +The modules `storage.bicep` and `app-config-store.bicep` use the Bicep `deployer().objectId` function to assign RBAC roles to the identity running the deployment. The `principalType` is hardcoded as `'User'`, which fails when the deployment is executed via a **managed identity** (which is of type `ServicePrincipal`). + +This affects AILZ-integrated deployments where `azd provision` is run from a jumpbox VM using `azd auth login --managed-identity`. + +**Affected Files:** + +### `infra/bicep/modules/storage.bicep` (lines ~72-80) + +**Before:** + +```bicep +var deployerRoleAssignments = [ + { + principalId: deployer().objectId + principalType: 'User' + roleDefinitionIdOrName: 'Storage Blob Data Contributor' + } + { + principalId: deployer().objectId + principalType: 'User' + roleDefinitionIdOrName: 'Storage Queue Data Contributor' + } + ] +``` + +**After:** + +```bicep +var deployerRoleAssignments = [ + { + principalId: deployer().objectId + principalType: deployer().objectType + roleDefinitionIdOrName: 'Storage Blob Data Contributor' + } + { + principalId: deployer().objectId + principalType: deployer().objectType + roleDefinitionIdOrName: 'Storage Queue Data Contributor' + } + ] +``` + +### `infra/bicep/modules/app-config-store.bicep` (lines ~36-41) + +**Before:** + +```bicep +var deployerRoleAssignments = [ + { + principalId: deployer().objectId + principalType: 'User' + roleDefinitionIdOrName: 'App Configuration Data Owner' + } + ] +``` + +**After:** + +```bicep +var deployerRoleAssignments = [ + { + principalId: deployer().objectId + principalType: deployer().objectType + roleDefinitionIdOrName: 'App Configuration Data Owner' + } + ] +``` + +**Explanation:** + +The `deployer()` function in Bicep returns both `.objectId` and `.objectType`. Using `deployer().objectType` instead of the hardcoded `'User'` string dynamically resolves the correct principal type (`User` for interactive logins, `ServicePrincipal` for managed identities), making the template work with both deployment methods. diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 47af02c..3c92a0f 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -445,7 +445,7 @@ module containerRegistry 'modules/container-registry.bicep' = { location: location roleAssignedManagedIdentityPrincipalIds: [userAssignedIdentity.outputs.principalId] enablePrivateEndpoint: isAILZIntegrated - privateEndpointSubnetId: isAILZIntegrated ? networkConfig.privateEndpointsSubnetId : '' + privateEndpointSubnetId: isAILZIntegrated ? networkConfig.privateEndpointSubnetId : '' acrPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.acr : '' publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' tags: tags @@ -460,7 +460,7 @@ module containerAppsEnvironment 'modules/container-apps-environment.bicep' = { name: 'cae-la-${resourceToken}' params: { containerAppsEnvironmentName: containerAppsEnvironmentName - logAnalyticsWorkspaceId: !empty(existingLogAnalyticsWorkspaceId) ? existingLogAnalyticsWorkspaceId : logAnalytics!.outputs.logAnalyticsWorkspaceId + logAnalyticsWorkspaceId: !empty(existingLogAnalyticsWorkspaceId) ? reference(existingLogAnalyticsWorkspaceId, '2021-12-01-preview').customerId : logAnalytics!.outputs.logAnalyticsWorkspaceId logAnalyticsPrimarySharedKey: !empty(existingLogAnalyticsWorkspaceId) ? listKeys(existingLogAnalyticsWorkspaceId, '2021-12-01-preview').primarySharedKey : logAnalytics.outputs.primarySharedKey userAssignedResourceIds: [userAssignedIdentity.outputs.resourceId] location: location From 63e73c105ba7d55bfdc74758854f1ac398e4a24e Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Thu, 19 Mar 2026 16:32:57 -0400 Subject: [PATCH 03/29] issues --- changes.md | 8 ++++---- infra/bicep/modules/app-config-store.bicep | 2 +- infra/bicep/modules/storage.bicep | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/changes.md b/changes.md index 51e6816..6ef49d9 100644 --- a/changes.md +++ b/changes.md @@ -119,12 +119,12 @@ var deployerRoleAssignments = [ var deployerRoleAssignments = [ { principalId: deployer().objectId - principalType: deployer().objectType + principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'Storage Blob Data Contributor' } { principalId: deployer().objectId - principalType: deployer().objectType + principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'Storage Queue Data Contributor' } ] @@ -150,7 +150,7 @@ var deployerRoleAssignments = [ var deployerRoleAssignments = [ { principalId: deployer().objectId - principalType: deployer().objectType + principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'App Configuration Data Owner' } ] @@ -158,4 +158,4 @@ var deployerRoleAssignments = [ **Explanation:** -The `deployer()` function in Bicep returns both `.objectId` and `.objectType`. Using `deployer().objectType` instead of the hardcoded `'User'` string dynamically resolves the correct principal type (`User` for interactive logins, `ServicePrincipal` for managed identities), making the template work with both deployment methods. +The `deployer()` function in Bicep only exposes three properties: `objectId`, `tenantId`, and `userPrincipalName`. For service principals and managed identities, `userPrincipalName` is empty. By checking `empty(deployer().userPrincipalName)`, we can dynamically determine the correct `principalType` — `'ServicePrincipal'` when UPN is empty (managed identity / SP), `'User'` when it has a value (interactive login). diff --git a/infra/bicep/modules/app-config-store.bicep b/infra/bicep/modules/app-config-store.bicep index 565e447..5a30330 100644 --- a/infra/bicep/modules/app-config-store.bicep +++ b/infra/bicep/modules/app-config-store.bicep @@ -35,7 +35,7 @@ var roleAssignments = [ var deployerRoleAssignments = [ { principalId: deployer().objectId - principalType: 'User' + principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'App Configuration Data Owner' } ] diff --git a/infra/bicep/modules/storage.bicep b/infra/bicep/modules/storage.bicep index 2553e2e..2ed5bd2 100644 --- a/infra/bicep/modules/storage.bicep +++ b/infra/bicep/modules/storage.bicep @@ -71,12 +71,12 @@ var queueRoleAssignments array = [for principalId in roleAssignedManagedIdentity var deployerRoleAssignments = [ { principalId: deployer().objectId - principalType: 'User' + principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'Storage Blob Data Contributor' } { principalId: deployer().objectId - principalType: 'User' + principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'Storage Queue Data Contributor' } ] From 3d2e950b60516635cc14ea609ba297117b4d5f5d Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Thu, 19 Mar 2026 17:21:24 -0400 Subject: [PATCH 04/29] issues fix --- changes.md | 15 +++++++++++---- infra/bicep/modules/app-config-store.bicep | 2 +- infra/bicep/modules/storage.bicep | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/changes.md b/changes.md index 6ef49d9..5d8ec2b 100644 --- a/changes.md +++ b/changes.md @@ -119,12 +119,12 @@ var deployerRoleAssignments = [ var deployerRoleAssignments = [ { principalId: deployer().objectId - principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' + principalType: empty(deployer().?userPrincipalName ?? '') ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'Storage Blob Data Contributor' } { principalId: deployer().objectId - principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' + principalType: empty(deployer().?userPrincipalName ?? '') ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'Storage Queue Data Contributor' } ] @@ -150,7 +150,7 @@ var deployerRoleAssignments = [ var deployerRoleAssignments = [ { principalId: deployer().objectId - principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' + principalType: empty(deployer().?userPrincipalName ?? '') ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'App Configuration Data Owner' } ] @@ -158,4 +158,11 @@ var deployerRoleAssignments = [ **Explanation:** -The `deployer()` function in Bicep only exposes three properties: `objectId`, `tenantId`, and `userPrincipalName`. For service principals and managed identities, `userPrincipalName` is empty. By checking `empty(deployer().userPrincipalName)`, we can dynamically determine the correct `principalType` — `'ServicePrincipal'` when UPN is empty (managed identity / SP), `'User'` when it has a value (interactive login). +The `deployer()` function returns `objectId` and `tenantId` for all principal types, but `userPrincipalName` is only included when the deployer is a user — it is **omitted entirely** (not just empty) for service principals and managed identities. Using `deployer().userPrincipalName` directly throws a runtime error because the property doesn't exist in the ARM response object. + +The fix uses the Bicep **safe-access operator** (`?.`) combined with the **null-coalescing operator** (`??`): +- `deployer().?userPrincipalName` → returns the value if it exists, or `null` if the property is absent +- `?? ''` → converts `null` to an empty string +- `empty(...)` → evaluates to `true` for managed identity/SP (no UPN), `false` for users (UPN present) + +This works correctly for both interactive user logins and managed identity deployments without requiring any additional parameters. diff --git a/infra/bicep/modules/app-config-store.bicep b/infra/bicep/modules/app-config-store.bicep index 5a30330..73428ca 100644 --- a/infra/bicep/modules/app-config-store.bicep +++ b/infra/bicep/modules/app-config-store.bicep @@ -35,7 +35,7 @@ var roleAssignments = [ var deployerRoleAssignments = [ { principalId: deployer().objectId - principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' + principalType: empty(deployer().?userPrincipalName ?? '') ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'App Configuration Data Owner' } ] diff --git a/infra/bicep/modules/storage.bicep b/infra/bicep/modules/storage.bicep index 2ed5bd2..d5d6d54 100644 --- a/infra/bicep/modules/storage.bicep +++ b/infra/bicep/modules/storage.bicep @@ -71,12 +71,12 @@ var queueRoleAssignments array = [for principalId in roleAssignedManagedIdentity var deployerRoleAssignments = [ { principalId: deployer().objectId - principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' + principalType: empty(deployer().?userPrincipalName ?? '') ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'Storage Blob Data Contributor' } { principalId: deployer().objectId - principalType: empty(deployer().userPrincipalName) ? 'ServicePrincipal' : 'User' + principalType: empty(deployer().?userPrincipalName ?? '') ? 'ServicePrincipal' : 'User' roleDefinitionIdOrName: 'Storage Queue Data Contributor' } ] From 9cb6ad7ced71bb8bd1bcf6d51d04b2c6693b4ade Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Thu, 19 Mar 2026 19:36:26 -0400 Subject: [PATCH 05/29] ACR issue --- changes.md | 68 +++++++++++++++++++ infra/README.md | 67 +++++++++++++++++-- infra/bicep/main.bicep | 88 ++++++++++++++++++++++--- infra/bicep/main.parameters.json | 3 + infra/bicep/modules/container-app.bicep | 7 +- 5 files changed, 219 insertions(+), 14 deletions(-) diff --git a/changes.md b/changes.md index 5d8ec2b..25ee81f 100644 --- a/changes.md +++ b/changes.md @@ -5,6 +5,7 @@ - [1. Fix: LogAnalytics CustomerId Must Be a GUID](#1-fix-loganalytics-customerid-must-be-a-guid) - [2. Fix: Property Name Typo `privateEndpointsSubnetId`](#2-fix-property-name-typo-privateendpointssubnetid) - [3. Fix: `UnmatchedPrincipalType` – `deployer().objectId` Hardcoded as `User`](#3-fix-unmatchedprincipaltype--deployerobjectid-hardcoded-as-user) +- [4. Feature: Support Existing Container Registry from AI Landing Zone](#4-feature-support-existing-container-registry-from-ai-landing-zone) --- @@ -166,3 +167,70 @@ The fix uses the Bicep **safe-access operator** (`?.`) combined with the **null- - `empty(...)` → evaluates to `true` for managed identity/SP (no UPN), `false` for users (UPN present) This works correctly for both interactive user logins and managed identity deployments without requiring any additional parameters. + +--- + +## 4. Feature: Support Existing Container Registry from AI Landing Zone + +**Problem:** + +In AILZ-integrated deployments without internet egress (no NAT Gateway), the Container Apps provisioning fails with a **20-minute timeout** because it cannot pull the hardcoded placeholder image `mcr.microsoft.com/azuredocs/containerapps-helloworld:latest` from the public Microsoft Container Registry (MCR). The private VNet has no outbound internet connectivity, making any MCR pull impossible. + +**Solution:** + +Added support for referencing an existing Azure Container Registry from the AI Landing Zone, following the same conditional pattern used for Log Analytics and App Insights. When an existing ACR is provided, the template: + +1. **Skips creating** a new Container Registry +2. **References the existing ACR** using the Bicep `existing` keyword +3. **Creates a private endpoint** for the existing ACR in the app's PE subnet (enables cross-VNet connectivity) +4. **Assigns RBAC** (AcrPull + AcrPush) to the managed identity on the existing ACR +5. **Uses a conditioned placeholder image** — MCR for basic mode (has internet), `${acr}/placeholder:latest` for AILZ (no internet) + +**Prerequisites for AILZ mode:** + +Before running `azd provision`, import a placeholder image into the existing ACR: +```bash +az acr import --name --source mcr.microsoft.com/k8se/quickstart:latest --image placeholder:latest +``` + +Then set the environment variable: +```bash +azd env set EXISTING_CONTAINER_REGISTRY_NAME "" +``` + +**Affected Files:** + +### `infra/bicep/main.bicep` + +- Added `existingContainerRegistryName` parameter (string, default empty) +- Added `shouldCreateContainerRegistry` conditional variable +- Wrapped existing `containerRegistry` module with `if (shouldCreateContainerRegistry)` +- Added `existing` resource reference for the ACR +- Added `containerRegistryLoginServer` and `containerRegistryNameResolved` variables +- Added private endpoint + DNS zone group for existing ACR (conditioned on AILZ mode) +- Added RBAC role assignments (AcrPull, AcrPush) on existing ACR for managed identity +- Updated all 3 container app module calls to use `containerRegistryLoginServer` instead of `containerRegistry!.outputs.loginServer` +- Added `placeholderImage` parameter to all 3 container app module calls +- Updated outputs to use resolved variables + +### `infra/bicep/main.parameters.json` + +Added parameter mapping: +```json +"existingContainerRegistryName": { + "value": "${EXISTING_CONTAINER_REGISTRY_NAME=}" +} +``` + +### `infra/bicep/modules/container-app.bicep` + +- Added `placeholderImage` parameter (string, default `mcr.microsoft.com/k8se/quickstart:latest`) +- Replaced hardcoded MCR image with `placeholderImage` parameter + +**Scenario Matrix:** + +| Mode | `EXISTING_CONTAINER_REGISTRY_NAME` | ACR Created | Placeholder Image | PE Created | +|------|-------------------------------------|-------------|-------------------|------------| +| basic | empty (default) | Yes (new) | `mcr.microsoft.com/k8se/quickstart:latest` | No | +| ailz-integrated | empty | Yes (new) | `mcr.microsoft.com/k8se/quickstart:latest` | Yes (new ACR) | +| ailz-integrated | `` | No (existing) | `/placeholder:latest` | Yes (existing ACR) | diff --git a/infra/README.md b/infra/README.md index d68178f..dcffc75 100644 --- a/infra/README.md +++ b/infra/README.md @@ -20,6 +20,7 @@ ContentFlow supports **two deployment configurations** based on your infrastruct | **Compliance Ready** | N/A | ✅ Yes | | **Enterprise Use** | Development | Production | | **Cost** | Lower | Slightly higher (Network infra costs added) | +| **Container Registry** | Creates new ACR | Can reuse existing AI LZ ACR | | **Prerequisites** | Azure subscription | Azure subscription + AI LZ | --- @@ -38,6 +39,7 @@ Before deploying, ensure you have: - Access to existing AI Landing Zone infrastructure - Documentation of VNet and DNS zone resource IDs - Appropriate permissions in the AI LZ resource group +- **(Recommended for no-egress VNets)** An existing Container Registry with a pre-imported placeholder image (see [Using an Existing Container Registry](#using-an-existing-container-registry-no-egress-vnets)) **Verify tools are installed:** ```shell @@ -220,6 +222,10 @@ azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$EXISTING_CONTAINER # (ContentFlow will create new ones if not provided) # azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$EXISTING_LOG_ANALYTICS_WORKSPACE_ID" # azd env set EXISTING_APP_INSIGHTS_ID "$EXISTING_APP_INSIGHTS_ID" + +# Optional: Use existing Container Registry (recommended for no-egress VNets) +# See "Using an Existing Container Registry" section below +# azd env set EXISTING_CONTAINER_REGISTRY_NAME "" ``` #### Step 4: Deploy ContentFlow @@ -259,7 +265,7 @@ This will: **What Gets Deployed:** - Container Apps with private endpoints -- Private Container Registry +- Container Registry (new, or private endpoint to existing ACR) - Storage Account with private endpoints - Cosmos DB with private endpoints - App Configuration with private endpoints @@ -280,6 +286,7 @@ This will: - Key Vault (optional) - Existing Log Analytics Workspace (optional) - Existing Application Insights (optional) +- Existing Container Registry (optional — recommended for no-egress VNets) **Duration:** 10-15 minutes (depends on existing AI LZ setup) @@ -412,6 +419,10 @@ azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID /subscriptions//resource azd env set EXISTING_APP_INSIGHTS_ID /subscriptions//resourceGroups//providers/Microsoft.Insights/components/ +# Optional: Use existing Container Registry (recommended for no-egress VNets) +# First import placeholder image: az acr import --name --source mcr.microsoft.com/k8se/quickstart:latest --image placeholder:latest +azd env set EXISTING_CONTAINER_REGISTRY_NAME + # Step 7: Deploy azd up ``` @@ -429,6 +440,53 @@ az network private-dns-zone list --resource-group --output table --- +### Using an Existing Container Registry (No-Egress VNets) + +In AILZ environments **without internet egress** (no NAT Gateway), Container Apps cannot pull placeholder images from the public Microsoft Container Registry (MCR) during initial provisioning. This causes a 20-minute timeout on the first deployment. + +To solve this, ContentFlow supports referencing an **existing Container Registry** from your AI Landing Zone — the same pattern used for Log Analytics and App Insights. + +#### Setup + +**1. Import a placeholder image** into your existing ACR (requires a machine with internet + ACR access): + +```bash +az acr import --name \ + --source mcr.microsoft.com/k8se/quickstart:latest \ + --image placeholder:latest +``` + +**2. Set the environment variable** before deploying: + +```bash +azd env set EXISTING_CONTAINER_REGISTRY_NAME "" +``` + +**3. Deploy normally:** + +```bash +azd up +``` + +#### What Happens + +When `EXISTING_CONTAINER_REGISTRY_NAME` is set: +- A new ACR is **not** created — the existing one is referenced +- A **private endpoint** is created for the existing ACR in the app's PE subnet (enables cross-VNet connectivity) +- **RBAC roles** (AcrPull + AcrPush) are assigned to the managed identity on the existing ACR +- Container Apps use `/placeholder:latest` instead of MCR — no internet required +- `azd deploy` pushes application images to the existing ACR via remote build + +| Scenario | ACR | Placeholder Image | Internet Required | +|----------|-----|-------------------|-------------------| +| Basic mode (default) | New ACR created | MCR public image | Yes | +| AILZ + no existing ACR | New ACR created | MCR public image | Yes | +| AILZ + existing ACR | Existing ACR reused | `/placeholder:latest` | **No** | + +> **Note:** The existing ACR must be in the **same subscription**. The deploying identity needs permissions to create role assignments on the existing ACR's resource group (Owner or User Access Administrator). + +--- + ## Structure ``` @@ -463,7 +521,7 @@ infra/ Main infrastructure template that provisions: - Resource Group (if not already existing) - Managed Identity (for Container Apps) -- Container Registry +- Container Registry (or references existing ACR with private endpoint and RBAC) - Container Apps Environment - Storage Account (blob + queue) - Cosmos DB with all required containers @@ -483,7 +541,7 @@ Each module is a reusable component: - **app-insights.bicep**: Application Insights for monitoring - **container-app.bicep**: Creates a Container App with configurable settings - **container-apps-environment.bicep**: Container Apps Environment -- **container-registry.bicep**: Azure Container Registry +- **container-registry.bicep**: Azure Container Registry (skipped when using existing ACR) - **cosmos-db.bicep**: Cosmos DB account with containers - **log-analytics-ws.bicep**: Log Analytics Workspace - **storage.bicep**: Storage Account with blob container and queue @@ -638,7 +696,8 @@ The infrastructure sets these key outputs: - `AZURE_LOCATION` - Azure region - `AZURE_RESOURCE_GROUP` - Resource group name -- `AZURE_CONTAINER_REGISTRY_ENDPOINT` - ACR login server +- `AZURE_CONTAINER_REGISTRY_ENDPOINT` - ACR login server (new or existing) +- `AZURE_CONTAINER_REGISTRY_NAME` - ACR name (new or existing) - `COSMOS_DB_ENDPOINT` - Cosmos DB endpoint - `STORAGE_ACCOUNT_NAME` - Storage account name - `APP_CONFIG_ENDPOINT` - App Configuration endpoint diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 3c92a0f..8d3625b 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -63,6 +63,9 @@ param existingLogAnalyticsWorkspaceId string = '' @description('Resource ID of existing App Insights from AI Landing Zone (optional, will create new if not provided)') param existingAppInsightsId string = '' +@description('Name of an existing Container Registry from AI Landing Zone (optional, will create new if not provided)') +param existingContainerRegistryName string = '' + // ========== APPLICATION SPECIFIC PARAMETERS ========== @description('Cosmos DB database name') param cosmosDbName string = 'contentflow' @@ -435,10 +438,10 @@ module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = { } // ========== CONTAINER REGISTRY WITH PRIVATE ENDPOINT SUPPORT ========== -// Create new Container Registry, with private endpoint support (if using AILZ integrated mode), -// or use public endpoints (basic mode) +// Use existing Container Registry if provided (AILZ), otherwise create a new one +var shouldCreateContainerRegistry = empty(existingContainerRegistryName) -module containerRegistry 'modules/container-registry.bicep' = { +module containerRegistry 'modules/container-registry.bicep' = if (shouldCreateContainerRegistry) { name: 'acr-${resourceToken}' params: { containerRegistryName: containerRegistryName @@ -452,6 +455,72 @@ module containerRegistry 'modules/container-registry.bicep' = { } } +// Reference existing Container Registry when provided +resource existingContainerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = if (!shouldCreateContainerRegistry) { + name: existingContainerRegistryName +} + +// Resolved ACR values that work for both existing and new registries +var containerRegistryLoginServer = shouldCreateContainerRegistry ? containerRegistry!.outputs.loginServer : existingContainerRegistry.properties.loginServer +var containerRegistryNameResolved = shouldCreateContainerRegistry ? containerRegistry!.outputs.name : existingContainerRegistryName + +// Private endpoint for existing ACR (needed when ACR is in a different VNet) +resource existingAcrPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (!shouldCreateContainerRegistry && isAILZIntegrated) { + name: '${existingContainerRegistryName}-pe' + location: location + tags: tags + properties: { + subnet: { + id: networkConfig.privateEndpointSubnetId + } + privateLinkServiceConnections: [ + { + name: '${existingContainerRegistryName}-acr-plsc' + properties: { + privateLinkServiceId: existingContainerRegistry.id + groupIds: ['registry'] + } + } + ] + } +} + +resource existingAcrDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = if (!shouldCreateContainerRegistry && isAILZIntegrated && !empty(existingAcrPrivateDnsZoneId)) { + parent: existingAcrPrivateEndpoint + name: 'acr-dns-zone-group' + properties: { + privateDnsZoneConfigs: [ + { + name: 'acr-config' + properties: { + privateDnsZoneId: existingAcrPrivateDnsZoneId + } + } + ] + } +} + +// RBAC for managed identity on existing ACR (AcrPull + AcrPush) +resource existingAcrRoleAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!shouldCreateContainerRegistry) { + name: guid(resourceGroup().id, existingContainerRegistryName, userAssignedIdentityName, '7f951dda-4ed3-4680-a7ca-43fe172d538d') + scope: existingContainerRegistry + properties: { + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull + } +} + +resource existingAcrRoleAcrPush 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!shouldCreateContainerRegistry) { + name: guid(resourceGroup().id, existingContainerRegistryName, userAssignedIdentityName, '8311e382-0749-4cb8-b61a-304f252e45ec') + scope: existingContainerRegistry + properties: { + principalId: userAssignedIdentity.outputs.principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec') // AcrPush + } +} + // ========== CONTAINER APPS ENVIRONMENT ========== // Create new Container Apps Environment, with private endpoint support (if using AILZ integrated mode), // or use public endpoints (basic mode) @@ -490,8 +559,9 @@ module apiContainerApp 'modules/container-app.bicep' = { name: apiContainerAppName location: containerAppsEnvironment!.outputs.location containerAppsEnvId: containerAppsEnvironment!.outputs.resourceId - containerRegistryServer: containerRegistry!.outputs.loginServer + containerRegistryServer: containerRegistryLoginServer managedIdentityId: userAssignedIdentity.outputs.resourceId + placeholderImage: shouldCreateContainerRegistry ? 'mcr.microsoft.com/k8se/quickstart:latest' : '${containerRegistryLoginServer}/placeholder:latest' targetPort: 8090 externalIngress: !isAILZIntegrated corsEnabled: true @@ -525,8 +595,9 @@ module workerContainerApp 'modules/container-app.bicep' = { name: workerContainerAppName location: containerAppsEnvironment!.outputs.location containerAppsEnvId: containerAppsEnvironment!.outputs.resourceId - containerRegistryServer: containerRegistry!.outputs.loginServer + containerRegistryServer: containerRegistryLoginServer managedIdentityId: userAssignedIdentity.outputs.resourceId + placeholderImage: shouldCreateContainerRegistry ? 'mcr.microsoft.com/k8se/quickstart:latest' : '${containerRegistryLoginServer}/placeholder:latest' targetPort: workerContainerAppTargetPort externalIngress: !isAILZIntegrated corsEnabled: true @@ -560,8 +631,9 @@ module webContainerApp 'modules/container-app.bicep' = { name: webContainerAppName location: containerAppsEnvironment!.outputs.location containerAppsEnvId: containerAppsEnvironment!.outputs.resourceId - containerRegistryServer: containerRegistry!.outputs.loginServer + containerRegistryServer: containerRegistryLoginServer managedIdentityId: userAssignedIdentity.outputs.resourceId + placeholderImage: shouldCreateContainerRegistry ? 'mcr.microsoft.com/k8se/quickstart:latest' : '${containerRegistryLoginServer}/placeholder:latest' targetPort: 8080 externalIngress: !isAILZIntegrated corsEnabled: true @@ -587,8 +659,8 @@ output AZURE_TENANT_ID string = tenant().tenantId output AZURE_RESOURCE_GROUP string = resourceGroup().name // Container Registry outputs -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry!.outputs.loginServer -output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistryLoginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistryNameResolved // Service endpoints output API_ENDPOINT string = 'https://${apiContainerApp.outputs.fqdn}' diff --git a/infra/bicep/main.parameters.json b/infra/bicep/main.parameters.json index 57ef45e..5442969 100644 --- a/infra/bicep/main.parameters.json +++ b/infra/bicep/main.parameters.json @@ -52,6 +52,9 @@ }, "existingAppInsightsId": { "value": "${EXISTING_APP_INSIGHTS_ID=}" + }, + "existingContainerRegistryName": { + "value": "${EXISTING_CONTAINER_REGISTRY_NAME=}" } } } diff --git a/infra/bicep/modules/container-app.bicep b/infra/bicep/modules/container-app.bicep index 2156c6d..0f5e511 100644 --- a/infra/bicep/modules/container-app.bicep +++ b/infra/bicep/modules/container-app.bicep @@ -46,6 +46,9 @@ param environmentVariables array = [] @description('Tags to apply to resources') param tags object = {} +@description('Placeholder image to use before azd deploy replaces it') +param placeholderImage string = 'mcr.microsoft.com/k8se/quickstart:latest' + // Use Azure Verified Module for Container App module containerApp 'br:mcr.microsoft.com/bicep/avm/res/app/container-app:0.19.0' = { @@ -66,8 +69,8 @@ module containerApp 'br:mcr.microsoft.com/bicep/avm/res/app/container-app:0.19.0 containers: [ { name: name - // Use base image as required by azd - will be replaced during deployment - image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest' + // Use placeholder image as required by azd - will be replaced during deployment + image: placeholderImage resources: { cpu: cpuCores memory: memoryInGB From 0ba266ce01da81eee0238a4be05813b94b5f99af Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Thu, 19 Mar 2026 19:47:30 -0400 Subject: [PATCH 06/29] ACR issue --- changes.md | 26 +++++----- infra/README.md | 13 ++--- infra/bicep/main.bicep | 47 +++++++------------ infra/bicep/main.parameters.json | 4 +- infra/bicep/modules/acr-role-assignment.bicep | 33 +++++++++++++ 5 files changed, 75 insertions(+), 48 deletions(-) create mode 100644 infra/bicep/modules/acr-role-assignment.bicep diff --git a/changes.md b/changes.md index 25ee81f..17000c5 100644 --- a/changes.md +++ b/changes.md @@ -193,32 +193,36 @@ Before running `azd provision`, import a placeholder image into the existing ACR az acr import --name --source mcr.microsoft.com/k8se/quickstart:latest --image placeholder:latest ``` -Then set the environment variable: +Then set the environment variable (use the full resource ID): ```bash -azd env set EXISTING_CONTAINER_REGISTRY_NAME "" +azd env set EXISTING_CONTAINER_REGISTRY_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/" ``` **Affected Files:** ### `infra/bicep/main.bicep` -- Added `existingContainerRegistryName` parameter (string, default empty) +- Added `existingContainerRegistryResourceId` parameter (string, default empty) - Added `shouldCreateContainerRegistry` conditional variable +- Added `existingAcrName` and `existingAcrResourceGroup` derived from resource ID - Wrapped existing `containerRegistry` module with `if (shouldCreateContainerRegistry)` -- Added `existing` resource reference for the ACR -- Added `containerRegistryLoginServer` and `containerRegistryNameResolved` variables -- Added private endpoint + DNS zone group for existing ACR (conditioned on AILZ mode) -- Added RBAC role assignments (AcrPull, AcrPush) on existing ACR for managed identity +- Added `containerRegistryLoginServer` (deterministic `.azurecr.io`) and `containerRegistryNameResolved` variables +- Added private endpoint + DNS zone group for existing ACR (conditioned on AILZ mode), using the resource ID in `privateLinkServiceId` +- Added cross-resource-group RBAC module (`modules/acr-role-assignment.bicep`) with `scope: resourceGroup(existingAcrResourceGroup)` for AcrPull + AcrPush - Updated all 3 container app module calls to use `containerRegistryLoginServer` instead of `containerRegistry!.outputs.loginServer` - Added `placeholderImage` parameter to all 3 container app module calls - Updated outputs to use resolved variables +### `infra/bicep/modules/acr-role-assignment.bicep` (new file) + +New module for cross-resource-group RBAC assignment on an existing ACR. Deployed with `scope: resourceGroup(existingAcrResourceGroup)` so it can reference the ACR in its own resource group. Assigns AcrPull and AcrPush to the managed identity. + ### `infra/bicep/main.parameters.json` Added parameter mapping: ```json -"existingContainerRegistryName": { - "value": "${EXISTING_CONTAINER_REGISTRY_NAME=}" +"existingContainerRegistryResourceId": { + "value": "${EXISTING_CONTAINER_REGISTRY_RESOURCE_ID=}" } ``` @@ -229,8 +233,8 @@ Added parameter mapping: **Scenario Matrix:** -| Mode | `EXISTING_CONTAINER_REGISTRY_NAME` | ACR Created | Placeholder Image | PE Created | +| Mode | `EXISTING_CONTAINER_REGISTRY_RESOURCE_ID` | ACR Created | Placeholder Image | PE Created | |------|-------------------------------------|-------------|-------------------|------------| | basic | empty (default) | Yes (new) | `mcr.microsoft.com/k8se/quickstart:latest` | No | | ailz-integrated | empty | Yes (new) | `mcr.microsoft.com/k8se/quickstart:latest` | Yes (new ACR) | -| ailz-integrated | `` | No (existing) | `/placeholder:latest` | Yes (existing ACR) | +| ailz-integrated | `/subscriptions/.../registries/` | No (existing) | `/placeholder:latest` | Yes (existing ACR) | diff --git a/infra/README.md b/infra/README.md index dcffc75..158f925 100644 --- a/infra/README.md +++ b/infra/README.md @@ -40,6 +40,7 @@ Before deploying, ensure you have: - Documentation of VNet and DNS zone resource IDs - Appropriate permissions in the AI LZ resource group - **(Recommended for no-egress VNets)** An existing Container Registry with a pre-imported placeholder image (see [Using an Existing Container Registry](#using-an-existing-container-registry-no-egress-vnets)) + - You will need the ACR's full **resource ID** (e.g., `/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/`) **Verify tools are installed:** ```shell @@ -225,7 +226,7 @@ azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$EXISTING_CONTAINER # Optional: Use existing Container Registry (recommended for no-egress VNets) # See "Using an Existing Container Registry" section below -# azd env set EXISTING_CONTAINER_REGISTRY_NAME "" +# azd env set EXISTING_CONTAINER_REGISTRY_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/" ``` #### Step 4: Deploy ContentFlow @@ -421,7 +422,7 @@ azd env set EXISTING_APP_INSIGHTS_ID /subscriptions//resourceGroups/ # Optional: Use existing Container Registry (recommended for no-egress VNets) # First import placeholder image: az acr import --name --source mcr.microsoft.com/k8se/quickstart:latest --image placeholder:latest -azd env set EXISTING_CONTAINER_REGISTRY_NAME +azd env set EXISTING_CONTAINER_REGISTRY_RESOURCE_ID /subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/ # Step 7: Deploy azd up @@ -456,10 +457,10 @@ az acr import --name \ --image placeholder:latest ``` -**2. Set the environment variable** before deploying: +**2. Set the environment variable** before deploying (use the full resource ID): ```bash -azd env set EXISTING_CONTAINER_REGISTRY_NAME "" +azd env set EXISTING_CONTAINER_REGISTRY_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/" ``` **3. Deploy normally:** @@ -470,10 +471,10 @@ azd up #### What Happens -When `EXISTING_CONTAINER_REGISTRY_NAME` is set: +When `EXISTING_CONTAINER_REGISTRY_RESOURCE_ID` is set: - A new ACR is **not** created — the existing one is referenced - A **private endpoint** is created for the existing ACR in the app's PE subnet (enables cross-VNet connectivity) -- **RBAC roles** (AcrPull + AcrPush) are assigned to the managed identity on the existing ACR +- **RBAC roles** (AcrPull + AcrPush) are assigned to the managed identity on the existing ACR (cross-resource-group deployment) - Container Apps use `/placeholder:latest` instead of MCR — no internet required - `azd deploy` pushes application images to the existing ACR via remote build diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 8d3625b..cc2709a 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -63,8 +63,8 @@ param existingLogAnalyticsWorkspaceId string = '' @description('Resource ID of existing App Insights from AI Landing Zone (optional, will create new if not provided)') param existingAppInsightsId string = '' -@description('Name of an existing Container Registry from AI Landing Zone (optional, will create new if not provided)') -param existingContainerRegistryName string = '' +@description('Resource ID of an existing Container Registry from AI Landing Zone (optional, will create new if not provided)') +param existingContainerRegistryResourceId string = '' // ========== APPLICATION SPECIFIC PARAMETERS ========== @description('Cosmos DB database name') @@ -439,7 +439,11 @@ module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = { // ========== CONTAINER REGISTRY WITH PRIVATE ENDPOINT SUPPORT ========== // Use existing Container Registry if provided (AILZ), otherwise create a new one -var shouldCreateContainerRegistry = empty(existingContainerRegistryName) +var shouldCreateContainerRegistry = empty(existingContainerRegistryResourceId) + +// Derived values from existing ACR resource ID +var existingAcrName = !shouldCreateContainerRegistry ? last(split(existingContainerRegistryResourceId, '/')) : '' +var existingAcrResourceGroup = !shouldCreateContainerRegistry ? split(existingContainerRegistryResourceId, '/')[4] : resourceGroup().name module containerRegistry 'modules/container-registry.bicep' = if (shouldCreateContainerRegistry) { name: 'acr-${resourceToken}' @@ -455,18 +459,13 @@ module containerRegistry 'modules/container-registry.bicep' = if (shouldCreateCo } } -// Reference existing Container Registry when provided -resource existingContainerRegistry 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = if (!shouldCreateContainerRegistry) { - name: existingContainerRegistryName -} - // Resolved ACR values that work for both existing and new registries -var containerRegistryLoginServer = shouldCreateContainerRegistry ? containerRegistry!.outputs.loginServer : existingContainerRegistry.properties.loginServer -var containerRegistryNameResolved = shouldCreateContainerRegistry ? containerRegistry!.outputs.name : existingContainerRegistryName +var containerRegistryLoginServer = shouldCreateContainerRegistry ? containerRegistry!.outputs.loginServer : '${existingAcrName}.azurecr.io' +var containerRegistryNameResolved = shouldCreateContainerRegistry ? containerRegistry!.outputs.name : existingAcrName // Private endpoint for existing ACR (needed when ACR is in a different VNet) resource existingAcrPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (!shouldCreateContainerRegistry && isAILZIntegrated) { - name: '${existingContainerRegistryName}-pe' + name: '${existingAcrName}-pe' location: location tags: tags properties: { @@ -475,9 +474,9 @@ resource existingAcrPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-11- } privateLinkServiceConnections: [ { - name: '${existingContainerRegistryName}-acr-plsc' + name: '${existingAcrName}-acr-plsc' properties: { - privateLinkServiceId: existingContainerRegistry.id + privateLinkServiceId: existingContainerRegistryResourceId groupIds: ['registry'] } } @@ -501,23 +500,13 @@ resource existingAcrDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZ } // RBAC for managed identity on existing ACR (AcrPull + AcrPush) -resource existingAcrRoleAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!shouldCreateContainerRegistry) { - name: guid(resourceGroup().id, existingContainerRegistryName, userAssignedIdentityName, '7f951dda-4ed3-4680-a7ca-43fe172d538d') - scope: existingContainerRegistry - properties: { - principalId: userAssignedIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull - } -} - -resource existingAcrRoleAcrPush 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (!shouldCreateContainerRegistry) { - name: guid(resourceGroup().id, existingContainerRegistryName, userAssignedIdentityName, '8311e382-0749-4cb8-b61a-304f252e45ec') - scope: existingContainerRegistry - properties: { +// Deployed to the ACR's resource group via module scope (cross-RG) +module existingAcrRbac 'modules/acr-role-assignment.bicep' = if (!shouldCreateContainerRegistry) { + name: 'existingAcrRbac-${resourceToken}' + scope: resourceGroup(existingAcrResourceGroup) + params: { + containerRegistryName: existingAcrName principalId: userAssignedIdentity.outputs.principalId - principalType: 'ServicePrincipal' - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec') // AcrPush } } diff --git a/infra/bicep/main.parameters.json b/infra/bicep/main.parameters.json index 5442969..2d88b67 100644 --- a/infra/bicep/main.parameters.json +++ b/infra/bicep/main.parameters.json @@ -53,8 +53,8 @@ "existingAppInsightsId": { "value": "${EXISTING_APP_INSIGHTS_ID=}" }, - "existingContainerRegistryName": { - "value": "${EXISTING_CONTAINER_REGISTRY_NAME=}" + "existingContainerRegistryResourceId": { + "value": "${EXISTING_CONTAINER_REGISTRY_RESOURCE_ID=}" } } } diff --git a/infra/bicep/modules/acr-role-assignment.bicep b/infra/bicep/modules/acr-role-assignment.bicep new file mode 100644 index 0000000..bb3e232 --- /dev/null +++ b/infra/bicep/modules/acr-role-assignment.bicep @@ -0,0 +1,33 @@ +// ACR Role Assignment Module +// Assigns AcrPull and AcrPush roles to a managed identity on an existing Container Registry. +// Deployed as a cross-resource-group module when the existing ACR is in a different RG. + +@description('Name of the existing Container Registry') +param containerRegistryName string + +@description('Principal ID of the managed identity to assign roles to') +param principalId string + +resource existingAcr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = { + name: containerRegistryName +} + +resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(existingAcr.id, principalId, '7f951dda-4ed3-4680-a7ca-43fe172d538d') + scope: existingAcr + properties: { + principalId: principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull + } +} + +resource acrPushRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(existingAcr.id, principalId, '8311e382-0749-4cb8-b61a-304f252e45ec') + scope: existingAcr + properties: { + principalId: principalId + principalType: 'ServicePrincipal' + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec') // AcrPush + } +} From 6a635b389108ba982f3d1461794625dd0dfcc64e Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 10:47:50 -0400 Subject: [PATCH 07/29] ACR issue fix 2 --- azure.yaml | 17 +- changes.md | 92 +- infra/README.md | 81 +- infra/bicep/main.bicep | 78 +- infra/bicep/main.json | 50820 ++++++++++++++++ infra/bicep/main.parameters.json | 3 - infra/bicep/modules/acr-role-assignment.bicep | 33 - infra/bicep/modules/container-registry.bicep | 5 + infra/scripts/post-provision.sh | 26 + 9 files changed, 50954 insertions(+), 201 deletions(-) create mode 100644 infra/bicep/main.json delete mode 100644 infra/bicep/modules/acr-role-assignment.bicep diff --git a/azure.yaml b/azure.yaml index f782d62..45bcdc4 100644 --- a/azure.yaml +++ b/azure.yaml @@ -61,18 +61,11 @@ hooks: continueOnError: false # Run after infrastructure is provisioned - # postprovision: - # shell: sh - # run: ./infra/scripts/post-provision.sh - # interactive: false - # continueOnError: false - - # # Run before deploying all services - # predeploy: - # shell: sh - # run: ./infra/scripts/pre-deploy.sh - # interactive: false - # continueOnError: false + postprovision: + shell: sh + run: ./infra/scripts/post-provision.sh + interactive: false + continueOnError: false # Run after all services are deployed postdeploy: diff --git a/changes.md b/changes.md index 17000c5..db61e83 100644 --- a/changes.md +++ b/changes.md @@ -170,71 +170,79 @@ This works correctly for both interactive user logins and managed identity deplo --- -## 4. Feature: Support Existing Container Registry from AI Landing Zone +## 4. Feature: Automated ACR Placeholder Import for AILZ Deployments **Problem:** -In AILZ-integrated deployments without internet egress (no NAT Gateway), the Container Apps provisioning fails with a **20-minute timeout** because it cannot pull the hardcoded placeholder image `mcr.microsoft.com/azuredocs/containerapps-helloworld:latest` from the public Microsoft Container Registry (MCR). The private VNet has no outbound internet connectivity, making any MCR pull impossible. +In AILZ-integrated deployments without internet egress (no NAT Gateway), Container Apps provisioning previously failed with a **20-minute timeout** because it couldn't pull placeholder images from the public Microsoft Container Registry (MCR). The private VNet has no outbound internet connectivity. **Solution:** -Added support for referencing an existing Azure Container Registry from the AI Landing Zone, following the same conditional pattern used for Log Analytics and App Insights. When an existing ACR is provided, the template: +Implemented an automated approach that maintains **Zero Trust / workload isolation** principles: -1. **Skips creating** a new Container Registry -2. **References the existing ACR** using the Bicep `existing` keyword -3. **Creates a private endpoint** for the existing ACR in the app's PE subnet (enables cross-VNet connectivity) -4. **Assigns RBAC** (AcrPull + AcrPush) to the managed identity on the existing ACR -5. **Uses a conditioned placeholder image** — MCR for basic mode (has internet), `${acr}/placeholder:latest` for AILZ (no internet) +1. **Always create a new ACR per workload** — never share a central ACR across workloads (violates isolation) +2. **Enable trusted Azure services** — set `networkRuleBypassOptions: 'AzureServices'` on the new ACR, allowing server-side operations like `az acr import` even when the ACR has private endpoints and `publicNetworkAccess: Disabled` +3. **Automated postprovision hook** — after Bicep provisions infrastructure, automatically import the placeholder image: `mcr.microsoft.com/k8se/quickstart:latest` → `${acr}/placeholder:latest` +4. **Non-blocking design** — Container Apps use the ACA platform-cached image `mcr.microsoft.com/k8se/quickstart:latest` during provisioning. The import is defensive, ensuring subsequent operations can resolve the image from ACR. -**Prerequisites for AILZ mode:** +**How It Works:** -Before running `azd provision`, import a placeholder image into the existing ACR: -```bash -az acr import --name --source mcr.microsoft.com/k8se/quickstart:latest --image placeholder:latest -``` +The `postprovision` hook in [azure.yaml](azure.yaml) runs [post-provision.sh](infra/scripts/post-provision.sh) after Bicep completes: -Then set the environment variable (use the full resource ID): ```bash -azd env set EXISTING_CONTAINER_REGISTRY_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/" +# Conditional import (only in AILZ mode) +if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then + az acr import \ + --name "$ACR_NAME" \ + --source mcr.microsoft.com/k8se/quickstart:latest \ + --image placeholder:latest +fi ``` +- **Basic mode**: Skip import (ACR is public, MCR reachable) +- **AILZ mode**: Import runs server-side via trusted services bypass (no client-side Docker or internet needed) + **Affected Files:** ### `infra/bicep/main.bicep` -- Added `existingContainerRegistryResourceId` parameter (string, default empty) -- Added `shouldCreateContainerRegistry` conditional variable -- Added `existingAcrName` and `existingAcrResourceGroup` derived from resource ID -- Wrapped existing `containerRegistry` module with `if (shouldCreateContainerRegistry)` -- Added `containerRegistryLoginServer` (deterministic `.azurecr.io`) and `containerRegistryNameResolved` variables -- Added private endpoint + DNS zone group for existing ACR (conditioned on AILZ mode), using the resource ID in `privateLinkServiceId` -- Added cross-resource-group RBAC module (`modules/acr-role-assignment.bicep`) with `scope: resourceGroup(existingAcrResourceGroup)` for AcrPull + AcrPush -- Updated all 3 container app module calls to use `containerRegistryLoginServer` instead of `containerRegistry!.outputs.loginServer` -- Added `placeholderImage` parameter to all 3 container app module calls -- Updated outputs to use resolved variables +- **Removed** `existingContainerRegistryResourceId` parameter (and all related variables/logic) +- **Removed** conditional ACR creation — now always creates new ACR +- **Removed** private endpoint + RBAC for existing ACR +- **Added** `networkRuleBypassOptions: 'AzureServices'` parameter to `containerRegistry` module call +- **Simplified** container app module calls — removed conditional `placeholderImage` (use default `k8se/quickstart`) +- **Simplified** outputs — use direct `containerRegistry.outputs.*` -### `infra/bicep/modules/acr-role-assignment.bicep` (new file) +### `infra/bicep/main.parameters.json` -New module for cross-resource-group RBAC assignment on an existing ACR. Deployed with `scope: resourceGroup(existingAcrResourceGroup)` so it can reference the ACR in its own resource group. Assigns AcrPull and AcrPush to the managed identity. +- **Removed** `existingContainerRegistryResourceId` parameter mapping -### `infra/bicep/main.parameters.json` +### `infra/bicep/modules/container-registry.bicep` -Added parameter mapping: -```json -"existingContainerRegistryResourceId": { - "value": "${EXISTING_CONTAINER_REGISTRY_RESOURCE_ID=}" -} -``` +- **Added** `networkRuleBypassOptions` parameter (default: `'AzureServices'`) +- **Pass through** to AVM module for trusted services support ### `infra/bicep/modules/container-app.bicep` -- Added `placeholderImage` parameter (string, default `mcr.microsoft.com/k8se/quickstart:latest`) -- Replaced hardcoded MCR image with `placeholderImage` parameter +- **No changes** — already has correct default `placeholderImage: 'mcr.microsoft.com/k8se/quickstart:latest'` + +### `infra/bicep/modules/acr-role-assignment.bicep` + +- **DELETED** — no longer needed (no cross-RG ACR RBAC) + +### `azure.yaml` + +- **Uncommented** `postprovision` hook to enable automated import + +### `infra/scripts/post-provision.sh` + +- **Added** conditional ACR import logic for AILZ mode +- **Added** error handling with fallback instructions -**Scenario Matrix:** +**Benefits:** -| Mode | `EXISTING_CONTAINER_REGISTRY_RESOURCE_ID` | ACR Created | Placeholder Image | PE Created | -|------|-------------------------------------|-------------|-------------------|------------| -| basic | empty (default) | Yes (new) | `mcr.microsoft.com/k8se/quickstart:latest` | No | -| ailz-integrated | empty | Yes (new) | `mcr.microsoft.com/k8se/quickstart:latest` | Yes (new ACR) | -| ailz-integrated | `/subscriptions/.../registries/` | No (existing) | `/placeholder:latest` | Yes (existing ACR) | +✅ **Zero Trust compliance** — each workload has its own ACR, no shared central registry +✅ **`azd up` remains one command** — fully automated, no manual steps +✅ **Works from jumpbox without Docker** — `az acr import` is server-side +✅ **Non-blocking** — ACA platform caches `k8se/quickstart`, provisioning succeeds even if import fails +✅ **CI/CD ready** — no human intervention required diff --git a/infra/README.md b/infra/README.md index 158f925..436e91e 100644 --- a/infra/README.md +++ b/infra/README.md @@ -39,8 +39,6 @@ Before deploying, ensure you have: - Access to existing AI Landing Zone infrastructure - Documentation of VNet and DNS zone resource IDs - Appropriate permissions in the AI LZ resource group -- **(Recommended for no-egress VNets)** An existing Container Registry with a pre-imported placeholder image (see [Using an Existing Container Registry](#using-an-existing-container-registry-no-egress-vnets)) - - You will need the ACR's full **resource ID** (e.g., `/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/`) **Verify tools are installed:** ```shell @@ -223,10 +221,6 @@ azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$EXISTING_CONTAINER # (ContentFlow will create new ones if not provided) # azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$EXISTING_LOG_ANALYTICS_WORKSPACE_ID" # azd env set EXISTING_APP_INSIGHTS_ID "$EXISTING_APP_INSIGHTS_ID" - -# Optional: Use existing Container Registry (recommended for no-egress VNets) -# See "Using an Existing Container Registry" section below -# azd env set EXISTING_CONTAINER_REGISTRY_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/" ``` #### Step 4: Deploy ContentFlow @@ -287,7 +281,6 @@ This will: - Key Vault (optional) - Existing Log Analytics Workspace (optional) - Existing Application Insights (optional) -- Existing Container Registry (optional — recommended for no-egress VNets) **Duration:** 10-15 minutes (depends on existing AI LZ setup) @@ -420,10 +413,6 @@ azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID /subscriptions//resource azd env set EXISTING_APP_INSIGHTS_ID /subscriptions//resourceGroups//providers/Microsoft.Insights/components/ -# Optional: Use existing Container Registry (recommended for no-egress VNets) -# First import placeholder image: az acr import --name --source mcr.microsoft.com/k8se/quickstart:latest --image placeholder:latest -azd env set EXISTING_CONTAINER_REGISTRY_RESOURCE_ID /subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/ - # Step 7: Deploy azd up ``` @@ -441,50 +430,60 @@ az network private-dns-zone list --resource-group --output table --- -### Using an Existing Container Registry (No-Egress VNets) +### Automated ACR Placeholder Import (AILZ No-Egress VNets) -In AILZ environments **without internet egress** (no NAT Gateway), Container Apps cannot pull placeholder images from the public Microsoft Container Registry (MCR) during initial provisioning. This causes a 20-minute timeout on the first deployment. +In AILZ environments **without internet egress** (no NAT Gateway), Container Apps cannot pull placeholder images from the public Microsoft Container Registry (MCR) during initial provisioning. -To solve this, ContentFlow supports referencing an **existing Container Registry** from your AI Landing Zone — the same pattern used for Log Analytics and App Insights. +**ContentFlow solves this automatically:** -#### Setup +1. **Always creates a new ACR per workload** — maintains Zero Trust / workload isolation (never shares a central ACR) +2. **Enables trusted Azure services** — sets `networkRuleBypassOptions: 'AzureServices'` on the ACR, allowing server-side operations even when the ACR has private endpoints +3. **Automated postprovision hook** — after Bicep provisions infrastructure, automatically imports the placeholder: + ```bash + az acr import --name \ + --source mcr.microsoft.com/k8se/quickstart:latest \ + --image placeholder:latest + ``` +4. **Non-blocking design** — Container Apps use the ACA platform-cached image `k8se/quickstart` during provisioning; the import is defensive -**1. Import a placeholder image** into your existing ACR (requires a machine with internet + ACR access): +**Benefits:** -```bash -az acr import --name \ - --source mcr.microsoft.com/k8se/quickstart:latest \ - --image placeholder:latest -``` +✅ **Zero Trust compliance** — each workload has its own ACR +✅ **`azd up` remains one command** — fully automated, no manual steps +✅ **Works from jumpbox without Docker** — `az acr import` is server-side +✅ **Non-blocking** — ACA platform caches `k8se/quickstart`, provisioning succeeds even if import fails +✅ **CI/CD ready** — no human intervention required -**2. Set the environment variable** before deploying (use the full resource ID): +**How It Works:** -```bash -azd env set EXISTING_CONTAINER_REGISTRY_RESOURCE_ID "/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/" -``` - -**3. Deploy normally:** +The `postprovision` hook in [azure.yaml](../azure.yaml) runs [post-provision.sh](scripts/post-provision.sh) after Bicep completes: ```bash -azd up +# Conditional import (only in AILZ mode) +if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then + az acr import \ + --name "$ACR_NAME" \ + --source mcr.microsoft.com/k8se/quickstart:latest \ + --image placeholder:latest +fi ``` -#### What Happens +- **Basic mode**: Skip import (ACR is public, MCR reachable) +- **AILZ mode**: Import runs server-side via trusted services bypass (no client-side Docker or internet needed) -When `EXISTING_CONTAINER_REGISTRY_RESOURCE_ID` is set: -- A new ACR is **not** created — the existing one is referenced -- A **private endpoint** is created for the existing ACR in the app's PE subnet (enables cross-VNet connectivity) -- **RBAC roles** (AcrPull + AcrPush) are assigned to the managed identity on the existing ACR (cross-resource-group deployment) -- Container Apps use `/placeholder:latest` instead of MCR — no internet required -- `azd deploy` pushes application images to the existing ACR via remote build +**Troubleshooting:** -| Scenario | ACR | Placeholder Image | Internet Required | -|----------|-----|-------------------|-------------------| -| Basic mode (default) | New ACR created | MCR public image | Yes | -| AILZ + no existing ACR | New ACR created | MCR public image | Yes | -| AILZ + existing ACR | Existing ACR reused | `/placeholder:latest` | **No** | +If the import fails (rare), you can manually import from a machine with internet + ACR access: + +```bash +# Get ACR name from azd environment +ACR_NAME=$(azd env get-value AZURE_CONTAINER_REGISTRY_NAME) -> **Note:** The existing ACR must be in the **same subscription**. The deploying identity needs permissions to create role assignments on the existing ACR's resource group (Owner or User Access Administrator). +# Manual import +az acr import --name $ACR_NAME \ + --source mcr.microsoft.com/k8se/quickstart:latest \ + --image placeholder:latest +``` --- diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index cc2709a..0eddd91 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -63,9 +63,6 @@ param existingLogAnalyticsWorkspaceId string = '' @description('Resource ID of existing App Insights from AI Landing Zone (optional, will create new if not provided)') param existingAppInsightsId string = '' -@description('Resource ID of an existing Container Registry from AI Landing Zone (optional, will create new if not provided)') -param existingContainerRegistryResourceId string = '' - // ========== APPLICATION SPECIFIC PARAMETERS ========== @description('Cosmos DB database name') param cosmosDbName string = 'contentflow' @@ -438,14 +435,8 @@ module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = { } // ========== CONTAINER REGISTRY WITH PRIVATE ENDPOINT SUPPORT ========== -// Use existing Container Registry if provided (AILZ), otherwise create a new one -var shouldCreateContainerRegistry = empty(existingContainerRegistryResourceId) - -// Derived values from existing ACR resource ID -var existingAcrName = !shouldCreateContainerRegistry ? last(split(existingContainerRegistryResourceId, '/')) : '' -var existingAcrResourceGroup = !shouldCreateContainerRegistry ? split(existingContainerRegistryResourceId, '/')[4] : resourceGroup().name - -module containerRegistry 'modules/container-registry.bicep' = if (shouldCreateContainerRegistry) { +// Always create a new Container Registry per workload (Zero Trust / workload isolation) +module containerRegistry 'modules/container-registry.bicep' = { name: 'acr-${resourceToken}' params: { containerRegistryName: containerRegistryName @@ -455,61 +446,11 @@ module containerRegistry 'modules/container-registry.bicep' = if (shouldCreateCo privateEndpointSubnetId: isAILZIntegrated ? networkConfig.privateEndpointSubnetId : '' acrPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.acr : '' publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' + networkRuleBypassOptions: 'AzureServices' tags: tags } } -// Resolved ACR values that work for both existing and new registries -var containerRegistryLoginServer = shouldCreateContainerRegistry ? containerRegistry!.outputs.loginServer : '${existingAcrName}.azurecr.io' -var containerRegistryNameResolved = shouldCreateContainerRegistry ? containerRegistry!.outputs.name : existingAcrName - -// Private endpoint for existing ACR (needed when ACR is in a different VNet) -resource existingAcrPrivateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' = if (!shouldCreateContainerRegistry && isAILZIntegrated) { - name: '${existingAcrName}-pe' - location: location - tags: tags - properties: { - subnet: { - id: networkConfig.privateEndpointSubnetId - } - privateLinkServiceConnections: [ - { - name: '${existingAcrName}-acr-plsc' - properties: { - privateLinkServiceId: existingContainerRegistryResourceId - groupIds: ['registry'] - } - } - ] - } -} - -resource existingAcrDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = if (!shouldCreateContainerRegistry && isAILZIntegrated && !empty(existingAcrPrivateDnsZoneId)) { - parent: existingAcrPrivateEndpoint - name: 'acr-dns-zone-group' - properties: { - privateDnsZoneConfigs: [ - { - name: 'acr-config' - properties: { - privateDnsZoneId: existingAcrPrivateDnsZoneId - } - } - ] - } -} - -// RBAC for managed identity on existing ACR (AcrPull + AcrPush) -// Deployed to the ACR's resource group via module scope (cross-RG) -module existingAcrRbac 'modules/acr-role-assignment.bicep' = if (!shouldCreateContainerRegistry) { - name: 'existingAcrRbac-${resourceToken}' - scope: resourceGroup(existingAcrResourceGroup) - params: { - containerRegistryName: existingAcrName - principalId: userAssignedIdentity.outputs.principalId - } -} - // ========== CONTAINER APPS ENVIRONMENT ========== // Create new Container Apps Environment, with private endpoint support (if using AILZ integrated mode), // or use public endpoints (basic mode) @@ -548,9 +489,8 @@ module apiContainerApp 'modules/container-app.bicep' = { name: apiContainerAppName location: containerAppsEnvironment!.outputs.location containerAppsEnvId: containerAppsEnvironment!.outputs.resourceId - containerRegistryServer: containerRegistryLoginServer + containerRegistryServer: containerRegistry.outputs.loginServer managedIdentityId: userAssignedIdentity.outputs.resourceId - placeholderImage: shouldCreateContainerRegistry ? 'mcr.microsoft.com/k8se/quickstart:latest' : '${containerRegistryLoginServer}/placeholder:latest' targetPort: 8090 externalIngress: !isAILZIntegrated corsEnabled: true @@ -584,9 +524,8 @@ module workerContainerApp 'modules/container-app.bicep' = { name: workerContainerAppName location: containerAppsEnvironment!.outputs.location containerAppsEnvId: containerAppsEnvironment!.outputs.resourceId - containerRegistryServer: containerRegistryLoginServer + containerRegistryServer: containerRegistry.outputs.loginServer managedIdentityId: userAssignedIdentity.outputs.resourceId - placeholderImage: shouldCreateContainerRegistry ? 'mcr.microsoft.com/k8se/quickstart:latest' : '${containerRegistryLoginServer}/placeholder:latest' targetPort: workerContainerAppTargetPort externalIngress: !isAILZIntegrated corsEnabled: true @@ -620,9 +559,8 @@ module webContainerApp 'modules/container-app.bicep' = { name: webContainerAppName location: containerAppsEnvironment!.outputs.location containerAppsEnvId: containerAppsEnvironment!.outputs.resourceId - containerRegistryServer: containerRegistryLoginServer + containerRegistryServer: containerRegistry.outputs.loginServer managedIdentityId: userAssignedIdentity.outputs.resourceId - placeholderImage: shouldCreateContainerRegistry ? 'mcr.microsoft.com/k8se/quickstart:latest' : '${containerRegistryLoginServer}/placeholder:latest' targetPort: 8080 externalIngress: !isAILZIntegrated corsEnabled: true @@ -648,8 +586,8 @@ output AZURE_TENANT_ID string = tenant().tenantId output AZURE_RESOURCE_GROUP string = resourceGroup().name // Container Registry outputs -output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistryLoginServer -output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistryNameResolved +output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.loginServer +output AZURE_CONTAINER_REGISTRY_NAME string = containerRegistry.outputs.name // Service endpoints output API_ENDPOINT string = 'https://${apiContainerApp.outputs.fqdn}' diff --git a/infra/bicep/main.json b/infra/bicep/main.json new file mode 100644 index 0000000..738c0ab --- /dev/null +++ b/infra/bicep/main.json @@ -0,0 +1,50820 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "5843273097420423186" + } + }, + "parameters": { + "deploymentMode": { + "type": "string", + "allowedValues": [ + "basic", + "ailz-integrated" + ], + "metadata": { + "description": "Deployment mode: basic (creates all networking) or ailz-integrated (uses existing AI Landing Zone)" + } + }, + "environmentName": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "metadata": { + "description": "Name of the environment that can be used as part of naming resource convention" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "minLength": 1, + "metadata": { + "description": "Primary location for all resources, defaults to resource group location" + } + }, + "foundryLocation": { + "type": "string", + "metadata": { + "description": "Location for AI Foundry resources" + } + }, + "principalId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "ID of the user running the deployment" + } + }, + "existingVnetResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing AI Landing Zone Virtual Network (required for ailz-integrated mode)" + } + }, + "privateEndpointSubnetName": { + "type": "string", + "defaultValue": "pe-subnet", + "metadata": { + "description": "Name of the subnet for private endpoints in AI Landing Zone VNet (default: pe-subnet)" + } + }, + "containerAppsSubnetName": { + "type": "string", + "defaultValue": "aca-env-subnet", + "metadata": { + "description": "Name of the subnet for container apps in AI Landing Zone VNet (default: aca-env-subnet)" + } + }, + "existingCognitiveServicesPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing Cognitive Services Private DNS Zone (required for ailz-integrated mode)" + } + }, + "existingBlobPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing Blob Storage Private DNS Zone (required for ailz-integrated mode)" + } + }, + "existingCosmosPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing Cosmos DB Private DNS Zone (required for ailz-integrated mode)" + } + }, + "existingAppConfigPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing App Config Private DNS Zone (required for ailz-integrated mode)" + } + }, + "existingAcrPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing Container Registry Private DNS Zone (required for ailz-integrated mode)" + } + }, + "existingContainerAppsEnvPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing Container Apps Environment Private DNS Zone (required for ailz-integrated mode)" + } + }, + "existingKeyVaultPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing Key Vault Private DNS Zone (optional)" + } + }, + "existingLogAnalyticsWorkspaceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing Log Analytics Workspace from AI Landing Zone (optional, will create new if not provided)" + } + }, + "existingAppInsightsId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Resource ID of existing App Insights from AI Landing Zone (optional, will create new if not provided)" + } + }, + "cosmosDbName": { + "type": "string", + "defaultValue": "contentflow", + "metadata": { + "description": "Cosmos DB database name" + } + }, + "cosmosDBContainerNames": { + "type": "array", + "defaultValue": [ + { + "name": "executor_catalog", + "partitionKey": "/id" + }, + { + "name": "pipelines", + "partitionKey": "/id" + }, + { + "name": "vaults", + "partitionKey": "/id" + }, + { + "name": "pipeline_executions", + "partitionKey": "/id" + }, + { + "name": "vault_exec_locks", + "partitionKey": "/id" + }, + { + "name": "vault_crawl_checkpoints", + "partitionKey": "/id" + }, + { + "name": "vault_executions", + "partitionKey": "/id" + } + ], + "metadata": { + "description": "Cosmos DB container names to create" + } + }, + "docsContainerName": { + "type": "string", + "defaultValue": "content", + "metadata": { + "description": "Name of the blob storage container for documents" + } + }, + "apiContainerAppTargetPort": { + "type": "int", + "defaultValue": 8090, + "metadata": { + "description": "API Container App target port" + } + }, + "workerContainerAppTargetPort": { + "type": "int", + "defaultValue": 8099, + "metadata": { + "description": "Worker Container App target port" + } + }, + "workerAPIEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable API service in worker container app - used for health checks" + } + }, + "workerQueueName": { + "type": "string", + "defaultValue": "contentflow-execution-requests", + "metadata": { + "description": "Queue name for worker tasks" + } + } + }, + "variables": { + "isBasic": "[equals(parameters('deploymentMode'), 'basic')]", + "isAILZIntegrated": "[equals(parameters('deploymentMode'), 'ailz-integrated')]", + "ailzValidation": "[if(variables('isAILZIntegrated'), createObject('vnetRequired', coalesce(not(empty(parameters('existingVnetResourceId'))), fail('existingVnetResourceId is required for ailz-integrated mode')), 'peSubnetRequired', coalesce(not(empty(parameters('privateEndpointSubnetName'))), fail('privateEndpointSubnetName is required for ailz-integrated mode')), 'caSubnetRequired', coalesce(not(empty(parameters('containerAppsSubnetName'))), fail('containerAppsSubnetName is required for ailz-integrated mode')), 'cognitiveServicesPrivateDnsZoneRequired', coalesce(not(empty(parameters('existingCognitiveServicesPrivateDnsZoneId'))), fail('existingCognitiveServicesPrivateDnsZoneId is required for ailz-integrated mode')), 'blobPrivateDnsZoneRequired', coalesce(not(empty(parameters('existingBlobPrivateDnsZoneId'))), fail('existingBlobPrivateDnsZoneId is required for ailz-integrated mode')), 'cosmosPrivateDnsZoneRequired', coalesce(not(empty(parameters('existingCosmosPrivateDnsZoneId'))), fail('existingCosmosPrivateDnsZoneId is required for ailz-integrated mode')), 'appConfigPrivateDnsZoneRequired', coalesce(not(empty(parameters('existingAppConfigPrivateDnsZoneId'))), fail('existingAppConfigPrivateDnsZoneId is required for ailz-integrated mode')), 'acrPrivateDnsZoneRequired', coalesce(not(empty(parameters('existingAcrPrivateDnsZoneId'))), fail('existingAcrPrivateDnsZoneId is required for ailz-integrated mode')), 'containerAppsEnvPrivateDnsZoneRequired', coalesce(not(empty(parameters('existingContainerAppsEnvPrivateDnsZoneId'))), fail('existingContainerAppsEnvPrivateDnsZoneId is required for ailz-integrated mode'))), createObject())]", + "tags": { + "azd-env-name": "[parameters('environmentName')]", + "application": "contentflow", + "deploymentMode": "[parameters('deploymentMode')]" + }, + "resourceToken": "[uniqueString(resourceGroup().id, parameters('environmentName'), parameters('location'))]", + "networkConfig": "[if(variables('isAILZIntegrated'), createObject('enablePrivateEndpoints', true(), 'publicNetworkAccess', 'Disabled', 'vnetResourceId', parameters('existingVnetResourceId'), 'privateEndpointSubnetId', format('{0}/subnets/{1}', parameters('existingVnetResourceId'), parameters('privateEndpointSubnetName')), 'containerAppsSubnetId', format('{0}/subnets/{1}', parameters('existingVnetResourceId'), parameters('containerAppsSubnetName')), 'privateDnsZoneIds', createObject('cognitiveServices', parameters('existingCognitiveServicesPrivateDnsZoneId'), 'blob', parameters('existingBlobPrivateDnsZoneId'), 'cosmos', parameters('existingCosmosPrivateDnsZoneId'), 'appConfig', parameters('existingAppConfigPrivateDnsZoneId'), 'acr', parameters('existingAcrPrivateDnsZoneId'), 'keyVault', parameters('existingKeyVaultPrivateDnsZoneId'), 'containerAppsEnv', parameters('existingContainerAppsEnvPrivateDnsZoneId'))), createObject('enablePrivateEndpoints', false(), 'publicNetworkAccess', 'Enabled'))]", + "userAssignedIdentityName": "[format('id-{0}', variables('resourceToken'))]", + "logAnalyticsWorkspaceName": "[format('log-{0}', variables('resourceToken'))]", + "appInsightsName": "[format('appi-{0}', variables('resourceToken'))]", + "storageAccountName": "[take(format('st{0}', variables('resourceToken')), 24)]", + "cosmosAccountName": "[format('cosmos-{0}', variables('resourceToken'))]", + "appConfigStoreName": "[format('appcs-{0}', variables('resourceToken'))]", + "containerRegistryName": "[format('cr{0}', variables('resourceToken'))]", + "containerAppsEnvironmentName": "[format('cae-{0}', variables('resourceToken'))]", + "apiContainerAppName": "[format('api-{0}', variables('resourceToken'))]", + "workerContainerAppName": "[format('worker-{0}', variables('resourceToken'))]", + "webContainerAppName": "[format('web-{0}', variables('resourceToken'))]", + "shouldCreateLogAnalytics": "[and(variables('isBasic'), empty(parameters('existingLogAnalyticsWorkspaceId')))]", + "shouldCreateAppInsights": "[and(variables('isBasic'), empty(parameters('existingAppInsightsId')))]" + }, + "resources": { + "userAssignedIdentity": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('userAssignedIdentity-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "userAssignedIdentityName": { + "value": "[variables('userAssignedIdentityName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "1897219362538033607" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional: Location for all resources. Default is the resource group location" + } + }, + "userAssignedIdentityName": { + "type": "string", + "metadata": { + "description": "Required: User Assigned Identity name" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional: Tags for resources" + } + } + }, + "resources": { + "userAssignedIdentity": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('userAssignedIdentity-{0}', uniqueString('userAssignedIdentity', deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('userAssignedIdentityName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "16707109626832623586" + }, + "name": "User Assigned Identities", + "description": "This module deploys a User Assigned Identity." + }, + "definitions": { + "federatedIdentityCredentialType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the federated identity credential." + } + }, + "audiences": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The list of audiences that can appear in the issued token." + } + }, + "issuer": { + "type": "string", + "metadata": { + "description": "Required. The URL of the issuer to be trusted." + } + }, + "subject": { + "type": "string", + "metadata": { + "description": "Required. The identifier of the external identity." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the federated identity credential." + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the User Assigned Identity." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "federatedIdentityCredentials": { + "type": "array", + "items": { + "$ref": "#/definitions/federatedIdentityCredentialType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The federated identity credentials list to indicate which token from the external IdP should be trusted by your application. Federated identity credentials are supported on applications only. A maximum of 20 federated identity credentials can be added per application object." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Managed Identity Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e40ec5ca-96e0-45a2-b4ff-59039f2c2b59')]", + "Managed Identity Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f1a07417-d97a-45cb-824c-7a7467783830')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.managedidentity-userassignedidentity.{0}.{1}', replace('0.4.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "userAssignedIdentity": { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]" + }, + "userAssignedIdentity_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.ManagedIdentity/userAssignedIdentities/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "userAssignedIdentity_roleAssignments": { + "copy": { + "name": "userAssignedIdentity_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.ManagedIdentity/userAssignedIdentities/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "userAssignedIdentity_federatedIdentityCredentials": { + "copy": { + "name": "userAssignedIdentity_federatedIdentityCredentials", + "count": "[length(coalesce(parameters('federatedIdentityCredentials'), createArray()))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-UserMSI-FederatedIdentityCred-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('federatedIdentityCredentials'), createArray())[copyIndex()].name]" + }, + "userAssignedIdentityName": { + "value": "[parameters('name')]" + }, + "audiences": { + "value": "[coalesce(parameters('federatedIdentityCredentials'), createArray())[copyIndex()].audiences]" + }, + "issuer": { + "value": "[coalesce(parameters('federatedIdentityCredentials'), createArray())[copyIndex()].issuer]" + }, + "subject": { + "value": "[coalesce(parameters('federatedIdentityCredentials'), createArray())[copyIndex()].subject]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13656021764446440473" + }, + "name": "User Assigned Identity Federated Identity Credential", + "description": "This module deploys a User Assigned Identity Federated Identity Credential." + }, + "parameters": { + "userAssignedIdentityName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent user assigned identity. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret." + } + }, + "audiences": { + "type": "array", + "metadata": { + "description": "Required. The list of audiences that can appear in the issued token. Should be set to api://AzureADTokenExchange for Azure AD. It says what Microsoft identity platform should accept in the aud claim in the incoming token. This value represents Azure AD in your external identity provider and has no fixed value across identity providers - you might need to create a new application registration in your IdP to serve as the audience of this token." + } + }, + "issuer": { + "type": "string", + "metadata": { + "description": "Required. The URL of the issuer to be trusted. Must match the issuer claim of the external token being exchanged." + } + }, + "subject": { + "type": "string", + "metadata": { + "description": "Required. The identifier of the external software workload within the external identity provider. Like the audience value, it has no fixed format, as each IdP uses their own - sometimes a GUID, sometimes a colon delimited identifier, sometimes arbitrary strings. The value here must match the sub claim within the token presented to Azure AD." + } + } + }, + "resources": [ + { + "type": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials", + "apiVersion": "2024-11-30", + "name": "[format('{0}/{1}', parameters('userAssignedIdentityName'), parameters('name'))]", + "properties": { + "audiences": "[parameters('audiences')]", + "issuer": "[parameters('issuer')]", + "subject": "[parameters('subject')]" + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the federated identity credential." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the federated identity credential." + }, + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials', parameters('userAssignedIdentityName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the federated identity credential was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the user assigned identity." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the user assigned identity." + }, + "value": "[resourceId('Microsoft.ManagedIdentity/userAssignedIdentities', parameters('name'))]" + }, + "principalId": { + "type": "string", + "metadata": { + "description": "The principal ID (object ID) of the user assigned identity." + }, + "value": "[reference('userAssignedIdentity').principalId]" + }, + "clientId": { + "type": "string", + "metadata": { + "description": "The client ID (application ID) of the user assigned identity." + }, + "value": "[reference('userAssignedIdentity').clientId]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the user assigned identity was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('userAssignedIdentity', '2024-11-30', 'full').location]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "value": "[reference('userAssignedIdentity').outputs.name.value]" + }, + "resourceId": { + "type": "string", + "value": "[reference('userAssignedIdentity').outputs.resourceId.value]" + }, + "principalId": { + "type": "string", + "value": "[reference('userAssignedIdentity').outputs.principalId.value]" + }, + "clientId": { + "type": "string", + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" + } + } + } + } + }, + "logAnalytics": { + "condition": "[variables('shouldCreateLogAnalytics')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('logAnalytics-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "logAnalyticsWorkspaceName": { + "value": "[variables('logAnalyticsWorkspaceName')]" + }, + "roleAssignedManagedIdentityPrincipalIds": { + "value": [ + "[reference('userAssignedIdentity').outputs.principalId.value]" + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "1635074562357373664" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources" + } + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "metadata": { + "description": "Log Analytics workspace name" + } + }, + "roleAssignedManagedIdentityPrincipalIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "User Assigned Identity that be given access to the Log Analytics Workspace" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for resources" + } + } + }, + "resources": { + "logAnalytics": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('logAnalytics-{0}', uniqueString('logAnalytics', deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('logAnalyticsWorkspaceName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "skuName": { + "value": "PerGB2018" + }, + "dataRetention": { + "value": 30 + }, + "roleAssignments": { + "copy": [ + { + "name": "value", + "count": "[length(parameters('roleAssignedManagedIdentityPrincipalIds'))]", + "input": "[createObject('principalId', parameters('roleAssignedManagedIdentityPrincipalIds')[copyIndex('value')], 'principalType', 'ServicePrincipal', 'roleDefinitionIdOrName', 'Log Analytics Contributor')]" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.1.42791", + "templateHash": "1749032521457140145" + }, + "name": "Log Analytics Workspaces", + "description": "This module deploys a Log Analytics Workspace." + }, + "definitions": { + "diagnosticSettingType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "useThisWorkspace": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Instead of using an external reference, use the deployed instance as the target for its diagnostic settings. If set to `true`, the `workspaceResourceId` property is ignored." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + } + }, + "gallerySolutionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the solution.\nFor solutions authored by Microsoft, the name must be in the pattern: `SolutionType(WorkspaceName)`, for example: `AntiMalware(contoso-Logs)`.\nFor solutions authored by third parties, the name should be in the pattern: `SolutionType[WorkspaceName]`, for example `MySolution[contoso-Logs]`.\nThe solution type is case-sensitive." + } + }, + "plan": { + "$ref": "#/definitions/solutionPlanType", + "metadata": { + "description": "Required. Plan for solution object supported by the OperationsManagement resource provider." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Properties of the gallery solutions to be created in the log analytics workspace." + } + }, + "storageInsightsConfigType": { + "type": "object", + "properties": { + "storageAccountResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the storage account to be linked." + } + }, + "containers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The names of the blob containers that the workspace should read." + } + }, + "tables": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of tables to be read by the workspace." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Properties of the storage insights configuration." + } + }, + "linkedServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the linked service." + } + }, + "resourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource id of the resource that will be linked to the workspace. This should be used for linking resources which require read access." + } + }, + "writeAccessResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource id of the resource that will be linked to the workspace. This should be used for linking resources which require write access." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Properties of the linked service." + } + }, + "linkedStorageAccountType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the link." + } + }, + "storageAccountIds": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "metadata": { + "description": "Required. Linked storage accounts resources Ids." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Properties of the linked storage account." + } + }, + "savedSearchType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the saved search." + } + }, + "etag": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The ETag of the saved search. To override an existing saved search, use \"*\" or specify the current Etag." + } + }, + "category": { + "type": "string", + "metadata": { + "description": "Required. The category of the saved search. This helps the user to find a saved search faster." + } + }, + "displayName": { + "type": "string", + "metadata": { + "description": "Required. Display name for the search." + } + }, + "functionAlias": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The function alias if query serves as a function." + } + }, + "functionParameters": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The optional function parameters if query serves as a function. Value should be in the following format: 'param-name1:type1 = default_value1, param-name2:type2 = default_value2'. For more examples and proper syntax please refer to /azure/kusto/query/functions/user-defined-functions." + } + }, + "query": { + "type": "string", + "metadata": { + "description": "Required. The query expression for the saved search." + } + }, + "tags": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. The tags attached to the saved search." + } + }, + "version": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The version number of the query language. The current version is 2 and is the default." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Properties of the saved search." + } + }, + "dataExportType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the data export." + } + }, + "destination": { + "$ref": "#/definitions/destinationType", + "nullable": true, + "metadata": { + "description": "Optional. The destination of the data export." + } + }, + "enable": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the data export." + } + }, + "tableNames": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The list of table names to export." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Properties of the data export." + } + }, + "dataSourceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the data source." + } + }, + "kind": { + "type": "string", + "metadata": { + "description": "Required. The kind of data source." + } + }, + "linkedResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource id of the resource that will be linked to the workspace." + } + }, + "eventLogName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the event log to configure when kind is WindowsEvent." + } + }, + "eventTypes": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. The event types to configure when kind is WindowsEvent." + } + }, + "objectName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the object to configure when kind is WindowsPerformanceCounter or LinuxPerformanceObject." + } + }, + "instanceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the instance to configure when kind is WindowsPerformanceCounter or LinuxPerformanceObject." + } + }, + "intervalSeconds": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Interval in seconds to configure when kind is WindowsPerformanceCounter or LinuxPerformanceObject." + } + }, + "performanceCounters": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. List of counters to configure when the kind is LinuxPerformanceObject." + } + }, + "counterName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Counter name to configure when kind is WindowsPerformanceCounter." + } + }, + "state": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. State to configure when kind is IISLogs or LinuxSyslogCollection or LinuxPerformanceCollection." + } + }, + "syslogName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. System log to configure when kind is LinuxSyslog." + } + }, + "syslogSeverities": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Severities to configure when kind is LinuxSyslog." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.OperationalInsights/workspaces/dataSources@2025-02-01#properties/tags" + }, + "description": "Optional. Tags to configure in the resource." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Properties of the data source." + } + }, + "tableType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the table." + } + }, + "plan": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The plan for the table." + } + }, + "restoredLogs": { + "$ref": "#/definitions/restoredLogsType", + "nullable": true, + "metadata": { + "description": "Optional. The restored logs for the table." + } + }, + "schema": { + "$ref": "#/definitions/schemaType", + "nullable": true, + "metadata": { + "description": "Optional. The schema for the table." + } + }, + "searchResults": { + "$ref": "#/definitions/searchResultsType", + "nullable": true, + "metadata": { + "description": "Optional. The search results for the table." + } + }, + "retentionInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The retention in days for the table." + } + }, + "totalRetentionInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The total retention in days for the table." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The role assignments for the table." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Properties of the custom table." + } + }, + "workspaceFeaturesType": { + "type": "object", + "properties": { + "disableLocalAuth": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Disable Non-EntraID based Auth. Default is true." + } + }, + "enableDataExport": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Flag that indicate if data should be exported." + } + }, + "enableLogAccessUsingOnlyResourcePermissions": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable log access using only resource permissions. Default is false." + } + }, + "immediatePurgeDataOn30Days": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Flag that describes if we want to remove the data after 30 days." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Features of the workspace." + } + }, + "workspaceReplicationType": { + "type": "object", + "properties": { + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether the replication is enabled or not. When true, workspace configuration and data is replicated to the specified location." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The location to which the workspace is replicated. Required if replication is enabled." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Replication properties of the workspace." + } + }, + "_1.columnType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The column name." + } + }, + "type": { + "type": "string", + "allowedValues": [ + "boolean", + "dateTime", + "dynamic", + "guid", + "int", + "long", + "real", + "string" + ], + "metadata": { + "description": "Required. The column type." + } + }, + "dataTypeHint": { + "type": "string", + "allowedValues": [ + "armPath", + "guid", + "ip", + "uri" + ], + "nullable": true, + "metadata": { + "description": "Optional. The column data type logical hint." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The column description." + } + }, + "displayName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Column display name." + } + } + }, + "metadata": { + "description": "The parameters of the table column.", + "__bicep_imported_from!": { + "sourceTemplate": "table/main.bicep" + } + } + }, + "destinationType": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "metadata": { + "description": "Required. The destination resource ID." + } + }, + "metaData": { + "type": "object", + "properties": { + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Allows to define an Event Hub name. Not applicable when destination is Storage Account." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The destination metadata." + } + } + }, + "metadata": { + "description": "The data export destination properties.", + "__bicep_imported_from!": { + "sourceTemplate": "data-export/main.bicep" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "restoredLogsType": { + "type": "object", + "properties": { + "sourceTable": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The table to restore data from." + } + }, + "startRestoreTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The timestamp to start the restore from (UTC)." + } + }, + "endRestoreTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The timestamp to end the restore by (UTC)." + } + } + }, + "metadata": { + "description": "The parameters of the restore operation that initiated the table.", + "__bicep_imported_from!": { + "sourceTemplate": "table/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "schemaType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The table name." + } + }, + "columns": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.columnType" + }, + "metadata": { + "description": "Required. A list of table custom columns." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The table description." + } + }, + "displayName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The table display name." + } + } + }, + "metadata": { + "description": "The table schema.", + "__bicep_imported_from!": { + "sourceTemplate": "table/main.bicep" + } + } + }, + "searchResultsType": { + "type": "object", + "properties": { + "query": { + "type": "string", + "metadata": { + "description": "Required. The search job query." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The search description." + } + }, + "limit": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Limit the search job to return up to specified number of rows." + } + }, + "startSearchTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The timestamp to start the search from (UTC)." + } + }, + "endSearchTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The timestamp to end the search by (UTC)." + } + } + }, + "metadata": { + "description": "The parameters of the search job that initiated the table.", + "__bicep_imported_from!": { + "sourceTemplate": "table/main.bicep" + } + } + }, + "solutionPlanType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the solution to be created.\nFor solutions authored by Microsoft, the name must be in the pattern: `SolutionType(WorkspaceName)`, for example: `AntiMalware(contoso-Logs)`.\nFor solutions authored by third parties, it can be anything.\nThe solution type is case-sensitive.\nIf not provided, the value of the `name` parameter will be used." + } + }, + "product": { + "type": "string", + "metadata": { + "description": "Required. The product name of the deployed solution.\nFor Microsoft published gallery solution it should be `OMSGallery/{solutionType}`, for example `OMSGallery/AntiMalware`.\nFor a third party solution, it can be anything.\nThis is case sensitive." + } + }, + "publisher": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The publisher name of the deployed solution. For Microsoft published gallery solution, it is `Microsoft`, which is the default value." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/operations-management/solution:0.3.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Log Analytics workspace." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "skuName": { + "type": "string", + "defaultValue": "PerGB2018", + "allowedValues": [ + "CapacityReservation", + "Free", + "LACluster", + "PerGB2018", + "PerNode", + "Premium", + "Standalone", + "Standard" + ], + "metadata": { + "description": "Optional. The name of the SKU." + } + }, + "skuCapacityReservationLevel": { + "type": "int", + "defaultValue": 100, + "minValue": 100, + "maxValue": 5000, + "metadata": { + "description": "Optional. The capacity reservation level in GB for this workspace, when CapacityReservation sku is selected. Must be in increments of 100 between 100 and 5000." + } + }, + "storageInsightsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/storageInsightsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of storage accounts to be read by the workspace." + } + }, + "linkedServices": { + "type": "array", + "items": { + "$ref": "#/definitions/linkedServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of services to be linked." + } + }, + "linkedStorageAccounts": { + "type": "array", + "items": { + "$ref": "#/definitions/linkedStorageAccountType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. List of Storage Accounts to be linked. Required if 'forceCmkForQuery' is set to 'true' and 'savedSearches' is not empty." + } + }, + "savedSearches": { + "type": "array", + "items": { + "$ref": "#/definitions/savedSearchType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Kusto Query Language searches to save." + } + }, + "dataExports": { + "type": "array", + "items": { + "$ref": "#/definitions/dataExportType" + }, + "nullable": true, + "metadata": { + "description": "Optional. LAW data export instances to be deployed." + } + }, + "dataSources": { + "type": "array", + "items": { + "$ref": "#/definitions/dataSourceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. LAW data sources to configure." + } + }, + "tables": { + "type": "array", + "items": { + "$ref": "#/definitions/tableType" + }, + "nullable": true, + "metadata": { + "description": "Optional. LAW custom tables to be deployed." + } + }, + "gallerySolutions": { + "type": "array", + "items": { + "$ref": "#/definitions/gallerySolutionType" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of gallerySolutions to be created in the log analytics workspace." + } + }, + "onboardWorkspaceToSentinel": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Onboard the Log Analytics Workspace to Sentinel. Requires 'SecurityInsights' solution to be in gallerySolutions." + } + }, + "dataRetention": { + "type": "int", + "defaultValue": 365, + "minValue": 0, + "maxValue": 730, + "metadata": { + "description": "Optional. Number of days data will be retained for." + } + }, + "dailyQuotaGb": { + "type": "int", + "defaultValue": -1, + "minValue": -1, + "metadata": { + "description": "Optional. The workspace daily quota for ingestion." + } + }, + "publicNetworkAccessForIngestion": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. The network access type for accessing Log Analytics ingestion." + } + }, + "publicNetworkAccessForQuery": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. The network access type for accessing Log Analytics query." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource. Only one type of identity is supported: system-assigned or user-assigned, but not both." + } + }, + "features": { + "$ref": "#/definitions/workspaceFeaturesType", + "nullable": true, + "metadata": { + "description": "Optional. The workspace features." + } + }, + "replication": { + "$ref": "#/definitions/workspaceReplicationType", + "nullable": true, + "metadata": { + "description": "Optional. The workspace replication properties." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "forceCmkForQuery": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether customer managed storage is mandatory for query management." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.OperationalInsights/workspaces@2025-02-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), 'SystemAssigned', if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Log Analytics Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '92aaf0da-9dab-42b6-94a3-d43ce8d16293')]", + "Log Analytics Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '73c42c96-874c-492b-b04d-ab87d138a893')]", + "Monitoring Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '749f88d5-cbae-40b8-bcfc-e573ddc772fa')]", + "Monitoring Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '43d0d8ad-25c7-4714-9337-8ba259a9fe05')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Security Admin": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fb1c8493-542b-48eb-b624-b4c8fea62acd')]", + "Security Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '39bc4728-0917-49c7-9d2c-d95423bc2eb4')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.operationalinsights-workspace.{0}.{1}', replace('0.12.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "logAnalyticsWorkspace": { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-02-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "features": { + "searchVersion": 1, + "enableLogAccessUsingOnlyResourcePermissions": "[coalesce(tryGet(parameters('features'), 'enableLogAccessUsingOnlyResourcePermissions'), false())]", + "disableLocalAuth": "[coalesce(tryGet(parameters('features'), 'disableLocalAuth'), true())]", + "enableDataExport": "[tryGet(parameters('features'), 'enableDataExport')]", + "immediatePurgeDataOn30Days": "[tryGet(parameters('features'), 'immediatePurgeDataOn30Days')]" + }, + "sku": { + "name": "[parameters('skuName')]", + "capacityReservationLevel": "[if(equals(parameters('skuName'), 'CapacityReservation'), parameters('skuCapacityReservationLevel'), null())]" + }, + "retentionInDays": "[parameters('dataRetention')]", + "workspaceCapping": { + "dailyQuotaGb": "[parameters('dailyQuotaGb')]" + }, + "publicNetworkAccessForIngestion": "[parameters('publicNetworkAccessForIngestion')]", + "publicNetworkAccessForQuery": "[parameters('publicNetworkAccessForQuery')]", + "forceCmkForQuery": "[parameters('forceCmkForQuery')]", + "replication": "[parameters('replication')]" + }, + "identity": "[variables('identity')]" + }, + "logAnalyticsWorkspace_diagnosticSettings": { + "copy": { + "name": "logAnalyticsWorkspace_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.OperationalInsights/workspaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[if(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'useThisWorkspace'), false()), resourceId('Microsoft.OperationalInsights/workspaces', parameters('name')), tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId'))]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_sentinelOnboarding": { + "condition": "[and(not(empty(filter(coalesce(parameters('gallerySolutions'), createArray()), lambda('item', startsWith(lambdaVariables('item').name, 'SecurityInsights'))))), parameters('onboardWorkspaceToSentinel'))]", + "type": "Microsoft.SecurityInsights/onboardingStates", + "apiVersion": "2024-03-01", + "scope": "[format('Microsoft.OperationalInsights/workspaces/{0}', parameters('name'))]", + "name": "default", + "properties": {}, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.OperationalInsights/workspaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_roleAssignments": { + "copy": { + "name": "logAnalyticsWorkspace_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.OperationalInsights/workspaces/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.OperationalInsights/workspaces', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_storageInsightConfigs": { + "copy": { + "name": "logAnalyticsWorkspace_storageInsightConfigs", + "count": "[length(coalesce(parameters('storageInsightsConfigs'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-LAW-StorageInsightsConfig-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "logAnalyticsWorkspaceName": { + "value": "[parameters('name')]" + }, + "containers": { + "value": "[tryGet(coalesce(parameters('storageInsightsConfigs'), createArray())[copyIndex()], 'containers')]" + }, + "tables": { + "value": "[tryGet(coalesce(parameters('storageInsightsConfigs'), createArray())[copyIndex()], 'tables')]" + }, + "storageAccountResourceId": { + "value": "[coalesce(parameters('storageInsightsConfigs'), createArray())[copyIndex()].storageAccountResourceId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.1.42791", + "templateHash": "1306323182548882150" + }, + "name": "Log Analytics Workspace Storage Insight Configs", + "description": "This module deploys a Log Analytics Workspace Storage Insight Config." + }, + "parameters": { + "logAnalyticsWorkspaceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Log Analytics workspace. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "[format('{0}-stinsconfig', last(split(parameters('storageAccountResourceId'), '/')))]", + "metadata": { + "description": "Optional. The name of the storage insights config." + } + }, + "storageAccountResourceId": { + "type": "string", + "metadata": { + "description": "Required. The Azure Resource Manager ID of the storage account resource." + } + }, + "containers": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The names of the blob containers that the workspace should read." + } + }, + "tables": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The names of the Azure tables that the workspace should read." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.OperationalInsights/workspaces/storageInsightConfigs@2025-02-01#properties/tags" + }, + "description": "Optional. Tags to configure in the resource." + }, + "nullable": true + } + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[last(split(parameters('storageAccountResourceId'), '/'))]" + }, + "workspace": { + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-02-01", + "name": "[parameters('logAnalyticsWorkspaceName')]" + }, + "storageinsightconfig": { + "type": "Microsoft.OperationalInsights/workspaces/storageInsightConfigs", + "apiVersion": "2025-02-01", + "name": "[format('{0}/{1}', parameters('logAnalyticsWorkspaceName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "containers": "[parameters('containers')]", + "tables": "[parameters('tables')]", + "storageAccount": { + "id": "[parameters('storageAccountResourceId')]", + "key": "[listKeys('storageAccount', '2024-01-01').keys[0].value]" + } + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed storage insights configuration." + }, + "value": "[resourceId('Microsoft.OperationalInsights/workspaces/storageInsightConfigs', parameters('logAnalyticsWorkspaceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group where the storage insight configuration is deployed." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the storage insights configuration." + }, + "value": "[parameters('name')]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_linkedServices": { + "copy": { + "name": "logAnalyticsWorkspace_linkedServices", + "count": "[length(coalesce(parameters('linkedServices'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-LAW-LinkedService-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "logAnalyticsWorkspaceName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('linkedServices'), createArray())[copyIndex()].name]" + }, + "resourceId": { + "value": "[tryGet(coalesce(parameters('linkedServices'), createArray())[copyIndex()], 'resourceId')]" + }, + "writeAccessResourceId": { + "value": "[tryGet(coalesce(parameters('linkedServices'), createArray())[copyIndex()], 'writeAccessResourceId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.1.42791", + "templateHash": "5230241501765697269" + }, + "name": "Log Analytics Workspace Linked Services", + "description": "This module deploys a Log Analytics Workspace Linked Service." + }, + "parameters": { + "logAnalyticsWorkspaceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Log Analytics workspace. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the link." + } + }, + "resourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the resource that will be linked to the workspace. This should be used for linking resources which require read access." + } + }, + "writeAccessResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the resource that will be linked to the workspace. This should be used for linking resources which require write access." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.OperationalInsights/workspaces/linkedServices@2025-02-01#properties/tags" + }, + "description": "Optional. Tags to configure in the resource." + }, + "nullable": true + } + }, + "resources": { + "workspace": { + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-02-01", + "name": "[parameters('logAnalyticsWorkspaceName')]" + }, + "linkedService": { + "type": "Microsoft.OperationalInsights/workspaces/linkedServices", + "apiVersion": "2025-02-01", + "name": "[format('{0}/{1}', parameters('logAnalyticsWorkspaceName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resourceId": "[parameters('resourceId')]", + "writeAccessResourceId": "[parameters('writeAccessResourceId')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed linked service." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed linked service." + }, + "value": "[resourceId('Microsoft.OperationalInsights/workspaces/linkedServices', parameters('logAnalyticsWorkspaceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group where the linked service is deployed." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_linkedStorageAccounts": { + "copy": { + "name": "logAnalyticsWorkspace_linkedStorageAccounts", + "count": "[length(coalesce(parameters('linkedStorageAccounts'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-LAW-LinkedStorageAccount-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "logAnalyticsWorkspaceName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('linkedStorageAccounts'), createArray())[copyIndex()].name]" + }, + "storageAccountIds": { + "value": "[coalesce(parameters('linkedStorageAccounts'), createArray())[copyIndex()].storageAccountIds]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.1.42791", + "templateHash": "10372135754202496594" + }, + "name": "Log Analytics Workspace Linked Storage Accounts", + "description": "This module deploys a Log Analytics Workspace Linked Storage Account." + }, + "parameters": { + "logAnalyticsWorkspaceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Log Analytics workspace. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "allowedValues": [ + "Query", + "Alerts", + "CustomLogs", + "AzureWatson" + ], + "metadata": { + "description": "Required. Name of the link." + } + }, + "storageAccountIds": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "metadata": { + "description": "Required. Linked storage accounts resources Ids." + } + } + }, + "resources": { + "workspace": { + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-02-01", + "name": "[parameters('logAnalyticsWorkspaceName')]" + }, + "linkedStorageAccount": { + "type": "Microsoft.OperationalInsights/workspaces/linkedStorageAccounts", + "apiVersion": "2025-02-01", + "name": "[format('{0}/{1}', parameters('logAnalyticsWorkspaceName'), parameters('name'))]", + "properties": { + "storageAccountIds": "[parameters('storageAccountIds')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed linked storage account." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed linked storage account." + }, + "value": "[resourceId('Microsoft.OperationalInsights/workspaces/linkedStorageAccounts', parameters('logAnalyticsWorkspaceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group where the linked storage account is deployed." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_savedSearches": { + "copy": { + "name": "logAnalyticsWorkspace_savedSearches", + "count": "[length(coalesce(parameters('savedSearches'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-LAW-SavedSearch-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "logAnalyticsWorkspaceName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[format('{0}{1}', coalesce(parameters('savedSearches'), createArray())[copyIndex()].name, uniqueString(deployment().name))]" + }, + "etag": { + "value": "[tryGet(coalesce(parameters('savedSearches'), createArray())[copyIndex()], 'etag')]" + }, + "displayName": { + "value": "[coalesce(parameters('savedSearches'), createArray())[copyIndex()].displayName]" + }, + "category": { + "value": "[coalesce(parameters('savedSearches'), createArray())[copyIndex()].category]" + }, + "query": { + "value": "[coalesce(parameters('savedSearches'), createArray())[copyIndex()].query]" + }, + "functionAlias": { + "value": "[tryGet(coalesce(parameters('savedSearches'), createArray())[copyIndex()], 'functionAlias')]" + }, + "functionParameters": { + "value": "[tryGet(coalesce(parameters('savedSearches'), createArray())[copyIndex()], 'functionParameters')]" + }, + "tags": { + "value": "[tryGet(coalesce(parameters('savedSearches'), createArray())[copyIndex()], 'tags')]" + }, + "version": { + "value": "[tryGet(coalesce(parameters('savedSearches'), createArray())[copyIndex()], 'version')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.1.42791", + "templateHash": "9015459905306126128" + }, + "name": "Log Analytics Workspace Saved Searches", + "description": "This module deploys a Log Analytics Workspace Saved Search." + }, + "parameters": { + "logAnalyticsWorkspaceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Log Analytics workspace. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the saved search." + } + }, + "displayName": { + "type": "string", + "metadata": { + "description": "Required. Display name for the search." + } + }, + "category": { + "type": "string", + "metadata": { + "description": "Required. Query category." + } + }, + "query": { + "type": "string", + "metadata": { + "description": "Required. Kusto Query to be stored." + } + }, + "tags": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.OperationalInsights/workspaces/savedSearches@2025-02-01#properties/properties/properties/tags" + }, + "description": "Optional. Tags to configure in the resource." + }, + "nullable": true + }, + "functionAlias": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The function alias if query serves as a function." + } + }, + "functionParameters": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The optional function parameters if query serves as a function. Value should be in the following format: \"param-name1:type1 = default_value1, param-name2:type2 = default_value2\". For more examples and proper syntax please refer to /azure/kusto/query/functions/user-defined-functions." + } + }, + "version": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The version number of the query language." + } + }, + "etag": { + "type": "string", + "defaultValue": "*", + "metadata": { + "description": "Optional. The ETag of the saved search. To override an existing saved search, use \"*\" or specify the current Etag." + } + } + }, + "resources": { + "workspace": { + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-02-01", + "name": "[parameters('logAnalyticsWorkspaceName')]" + }, + "savedSearch": { + "type": "Microsoft.OperationalInsights/workspaces/savedSearches", + "apiVersion": "2025-02-01", + "name": "[format('{0}/{1}', parameters('logAnalyticsWorkspaceName'), parameters('name'))]", + "properties": { + "etag": "[parameters('etag')]", + "tags": "[coalesce(parameters('tags'), createArray())]", + "displayName": "[parameters('displayName')]", + "category": "[parameters('category')]", + "query": "[parameters('query')]", + "functionAlias": "[parameters('functionAlias')]", + "functionParameters": "[parameters('functionParameters')]", + "version": "[parameters('version')]" + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed saved search." + }, + "value": "[resourceId('Microsoft.OperationalInsights/workspaces/savedSearches', parameters('logAnalyticsWorkspaceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group where the saved search is deployed." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed saved search." + }, + "value": "[parameters('name')]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace", + "logAnalyticsWorkspace_linkedStorageAccounts" + ] + }, + "logAnalyticsWorkspace_dataExports": { + "copy": { + "name": "logAnalyticsWorkspace_dataExports", + "count": "[length(coalesce(parameters('dataExports'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-LAW-DataExport-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "workspaceName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('dataExports'), createArray())[copyIndex()].name]" + }, + "destination": { + "value": "[tryGet(coalesce(parameters('dataExports'), createArray())[copyIndex()], 'destination')]" + }, + "enable": { + "value": "[tryGet(coalesce(parameters('dataExports'), createArray())[copyIndex()], 'enable')]" + }, + "tableNames": { + "value": "[tryGet(coalesce(parameters('dataExports'), createArray())[copyIndex()], 'tableNames')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.1.42791", + "templateHash": "8586520532175356447" + }, + "name": "Log Analytics Workspace Data Exports", + "description": "This module deploys a Log Analytics Workspace Data Export." + }, + "definitions": { + "destinationType": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "metadata": { + "description": "Required. The destination resource ID." + } + }, + "metaData": { + "type": "object", + "properties": { + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Allows to define an Event Hub name. Not applicable when destination is Storage Account." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The destination metadata." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The data export destination properties." + } + } + }, + "parameters": { + "name": { + "type": "string", + "minLength": 4, + "maxLength": 63, + "metadata": { + "description": "Required. The data export rule name." + } + }, + "workspaceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent workspaces. Required if the template is used in a standalone deployment." + } + }, + "destination": { + "$ref": "#/definitions/destinationType", + "nullable": true, + "metadata": { + "description": "Optional. Destination properties." + } + }, + "enable": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Active when enabled." + } + }, + "tableNames": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "metadata": { + "description": "Required. An array of tables to export, for example: ['Heartbeat', 'SecurityEvent']." + } + } + }, + "resources": { + "workspace": { + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-02-01", + "name": "[parameters('workspaceName')]" + }, + "dataExport": { + "type": "Microsoft.OperationalInsights/workspaces/dataExports", + "apiVersion": "2025-02-01", + "name": "[format('{0}/{1}', parameters('workspaceName'), parameters('name'))]", + "properties": { + "destination": "[parameters('destination')]", + "enable": "[parameters('enable')]", + "tableNames": "[parameters('tableNames')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the data export." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the data export." + }, + "value": "[resourceId('Microsoft.OperationalInsights/workspaces/dataExports', parameters('workspaceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the data export was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_dataSources": { + "copy": { + "name": "logAnalyticsWorkspace_dataSources", + "count": "[length(coalesce(parameters('dataSources'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-LAW-DataSource-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "logAnalyticsWorkspaceName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('dataSources'), createArray())[copyIndex()].name]" + }, + "kind": { + "value": "[coalesce(parameters('dataSources'), createArray())[copyIndex()].kind]" + }, + "linkedResourceId": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'linkedResourceId')]" + }, + "eventLogName": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'eventLogName')]" + }, + "eventTypes": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'eventTypes')]" + }, + "objectName": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'objectName')]" + }, + "instanceName": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'instanceName')]" + }, + "intervalSeconds": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'intervalSeconds')]" + }, + "counterName": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'counterName')]" + }, + "state": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'state')]" + }, + "syslogName": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'syslogName')]" + }, + "syslogSeverities": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'syslogSeverities')]" + }, + "performanceCounters": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'performanceCounters')]" + }, + "tags": { + "value": "[tryGet(coalesce(parameters('dataSources'), createArray())[copyIndex()], 'tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.1.42791", + "templateHash": "8336916453932906250" + }, + "name": "Log Analytics Workspace Datasources", + "description": "This module deploys a Log Analytics Workspace Data Source." + }, + "parameters": { + "logAnalyticsWorkspaceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Log Analytics workspace. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the data source." + } + }, + "kind": { + "type": "string", + "defaultValue": "AzureActivityLog", + "allowedValues": [ + "AzureActivityLog", + "WindowsEvent", + "WindowsPerformanceCounter", + "IISLogs", + "LinuxSyslog", + "LinuxSyslogCollection", + "LinuxPerformanceObject", + "LinuxPerformanceCollection" + ], + "metadata": { + "description": "Optional. The kind of the data source." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.OperationalInsights/workspaces/dataSources@2025-02-01#properties/tags" + }, + "description": "Optional. Tags to configure in the resource." + }, + "nullable": true + }, + "linkedResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the resource to be linked." + } + }, + "eventLogName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Windows event log name to configure when kind is WindowsEvent." + } + }, + "eventTypes": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. Windows event types to configure when kind is WindowsEvent." + } + }, + "objectName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the object to configure when kind is WindowsPerformanceCounter or LinuxPerformanceObject." + } + }, + "instanceName": { + "type": "string", + "defaultValue": "*", + "metadata": { + "description": "Optional. Name of the instance to configure when kind is WindowsPerformanceCounter or LinuxPerformanceObject." + } + }, + "intervalSeconds": { + "type": "int", + "defaultValue": 60, + "metadata": { + "description": "Optional. Interval in seconds to configure when kind is WindowsPerformanceCounter or LinuxPerformanceObject." + } + }, + "performanceCounters": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. List of counters to configure when the kind is LinuxPerformanceObject." + } + }, + "counterName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Counter name to configure when kind is WindowsPerformanceCounter." + } + }, + "state": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. State to configure when kind is IISLogs or LinuxSyslogCollection or LinuxPerformanceCollection." + } + }, + "syslogName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. System log to configure when kind is LinuxSyslog." + } + }, + "syslogSeverities": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. Severities to configure when kind is LinuxSyslog." + } + } + }, + "resources": { + "workspace": { + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-02-01", + "name": "[parameters('logAnalyticsWorkspaceName')]" + }, + "dataSource": { + "type": "Microsoft.OperationalInsights/workspaces/dataSources", + "apiVersion": "2025-02-01", + "name": "[format('{0}/{1}', parameters('logAnalyticsWorkspaceName'), parameters('name'))]", + "kind": "[parameters('kind')]", + "tags": "[parameters('tags')]", + "properties": { + "linkedResourceId": "[if(and(not(empty(parameters('kind'))), equals(parameters('kind'), 'AzureActivityLog')), parameters('linkedResourceId'), null())]", + "eventLogName": "[if(and(not(empty(parameters('kind'))), equals(parameters('kind'), 'WindowsEvent')), parameters('eventLogName'), null())]", + "eventTypes": "[if(and(not(empty(parameters('kind'))), equals(parameters('kind'), 'WindowsEvent')), parameters('eventTypes'), null())]", + "objectName": "[if(and(not(empty(parameters('kind'))), or(equals(parameters('kind'), 'WindowsPerformanceCounter'), equals(parameters('kind'), 'LinuxPerformanceObject'))), parameters('objectName'), null())]", + "instanceName": "[if(and(not(empty(parameters('kind'))), or(equals(parameters('kind'), 'WindowsPerformanceCounter'), equals(parameters('kind'), 'LinuxPerformanceObject'))), parameters('instanceName'), null())]", + "intervalSeconds": "[if(and(not(empty(parameters('kind'))), or(equals(parameters('kind'), 'WindowsPerformanceCounter'), equals(parameters('kind'), 'LinuxPerformanceObject'))), parameters('intervalSeconds'), null())]", + "counterName": "[if(and(not(empty(parameters('kind'))), equals(parameters('kind'), 'WindowsPerformanceCounter')), parameters('counterName'), null())]", + "state": "[if(and(not(empty(parameters('kind'))), or(or(equals(parameters('kind'), 'IISLogs'), equals(parameters('kind'), 'LinuxSyslogCollection')), equals(parameters('kind'), 'LinuxPerformanceCollection'))), parameters('state'), null())]", + "syslogName": "[if(and(not(empty(parameters('kind'))), equals(parameters('kind'), 'LinuxSyslog')), parameters('syslogName'), null())]", + "syslogSeverities": "[if(and(not(empty(parameters('kind'))), or(equals(parameters('kind'), 'LinuxSyslog'), equals(parameters('kind'), 'LinuxPerformanceObject'))), parameters('syslogSeverities'), null())]", + "performanceCounters": "[if(and(not(empty(parameters('kind'))), equals(parameters('kind'), 'LinuxPerformanceObject')), parameters('performanceCounters'), null())]" + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed data source." + }, + "value": "[resourceId('Microsoft.OperationalInsights/workspaces/dataSources', parameters('logAnalyticsWorkspaceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group where the data source is deployed." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed data source." + }, + "value": "[parameters('name')]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_tables": { + "copy": { + "name": "logAnalyticsWorkspace_tables", + "count": "[length(coalesce(parameters('tables'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-LAW-Table-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "workspaceName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('tables'), createArray())[copyIndex()].name]" + }, + "plan": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'plan')]" + }, + "schema": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'schema')]" + }, + "retentionInDays": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'retentionInDays')]" + }, + "totalRetentionInDays": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'totalRetentionInDays')]" + }, + "restoredLogs": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'restoredLogs')]" + }, + "searchResults": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'searchResults')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.36.1.42791", + "templateHash": "315390662258960765" + }, + "name": "Log Analytics Workspace Tables", + "description": "This module deploys a Log Analytics Workspace Table." + }, + "definitions": { + "restoredLogsType": { + "type": "object", + "properties": { + "sourceTable": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The table to restore data from." + } + }, + "startRestoreTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The timestamp to start the restore from (UTC)." + } + }, + "endRestoreTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The timestamp to end the restore by (UTC)." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The parameters of the restore operation that initiated the table." + } + }, + "schemaType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The table name." + } + }, + "columns": { + "type": "array", + "items": { + "$ref": "#/definitions/columnType" + }, + "metadata": { + "description": "Required. A list of table custom columns." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The table description." + } + }, + "displayName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The table display name." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The table schema." + } + }, + "columnType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The column name." + } + }, + "type": { + "type": "string", + "allowedValues": [ + "boolean", + "dateTime", + "dynamic", + "guid", + "int", + "long", + "real", + "string" + ], + "metadata": { + "description": "Required. The column type." + } + }, + "dataTypeHint": { + "type": "string", + "allowedValues": [ + "armPath", + "guid", + "ip", + "uri" + ], + "nullable": true, + "metadata": { + "description": "Optional. The column data type logical hint." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The column description." + } + }, + "displayName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Column display name." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The parameters of the table column." + } + }, + "searchResultsType": { + "type": "object", + "properties": { + "query": { + "type": "string", + "metadata": { + "description": "Required. The search job query." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The search description." + } + }, + "limit": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Limit the search job to return up to specified number of rows." + } + }, + "startSearchTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The timestamp to start the search from (UTC)." + } + }, + "endSearchTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The timestamp to end the search by (UTC)." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The parameters of the search job that initiated the table." + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the table." + } + }, + "workspaceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent workspaces. Required if the template is used in a standalone deployment." + } + }, + "plan": { + "type": "string", + "defaultValue": "Analytics", + "allowedValues": [ + "Basic", + "Analytics" + ], + "metadata": { + "description": "Optional. Instruct the system how to handle and charge the logs ingested to this table." + } + }, + "restoredLogs": { + "$ref": "#/definitions/restoredLogsType", + "nullable": true, + "metadata": { + "description": "Optional. Restore parameters." + } + }, + "retentionInDays": { + "type": "int", + "defaultValue": -1, + "minValue": -1, + "maxValue": 730, + "metadata": { + "description": "Optional. The table retention in days, between 4 and 730. Setting this property to -1 will default to the workspace retention." + } + }, + "schema": { + "$ref": "#/definitions/schemaType", + "nullable": true, + "metadata": { + "description": "Optional. Table's schema." + } + }, + "searchResults": { + "$ref": "#/definitions/searchResultsType", + "nullable": true, + "metadata": { + "description": "Optional. Parameters of the search job that initiated this table." + } + }, + "totalRetentionInDays": { + "type": "int", + "defaultValue": -1, + "minValue": -1, + "maxValue": 2555, + "metadata": { + "description": "Optional. The table total retention in days, between 4 and 2555. Setting this property to -1 will default to table retention." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Log Analytics Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '92aaf0da-9dab-42b6-94a3-d43ce8d16293')]", + "Log Analytics Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '73c42c96-874c-492b-b04d-ab87d138a893')]", + "Monitoring Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '749f88d5-cbae-40b8-bcfc-e573ddc772fa')]", + "Monitoring Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '43d0d8ad-25c7-4714-9337-8ba259a9fe05')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "workspace": { + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2025-02-01", + "name": "[parameters('workspaceName')]" + }, + "table": { + "type": "Microsoft.OperationalInsights/workspaces/tables", + "apiVersion": "2025-02-01", + "name": "[format('{0}/{1}', parameters('workspaceName'), parameters('name'))]", + "properties": { + "plan": "[parameters('plan')]", + "restoredLogs": "[parameters('restoredLogs')]", + "retentionInDays": "[parameters('retentionInDays')]", + "schema": "[parameters('schema')]", + "searchResults": "[parameters('searchResults')]", + "totalRetentionInDays": "[parameters('totalRetentionInDays')]" + } + }, + "table_roleAssignments": { + "copy": { + "name": "table_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.OperationalInsights/workspaces/{0}/tables/{1}', parameters('workspaceName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.OperationalInsights/workspaces/tables', parameters('workspaceName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "table" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the table." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the table." + }, + "value": "[resourceId('Microsoft.OperationalInsights/workspaces/tables', parameters('workspaceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the table was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + }, + "logAnalyticsWorkspace_solutions": { + "copy": { + "name": "logAnalyticsWorkspace_solutions", + "count": "[length(coalesce(parameters('gallerySolutions'), createArray()))]" + }, + "condition": "[not(empty(parameters('gallerySolutions')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-LAW-Solution-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('gallerySolutions'), createArray())[copyIndex()].name]" + }, + "location": { + "value": "[parameters('location')]" + }, + "logAnalyticsWorkspaceName": { + "value": "[parameters('name')]" + }, + "plan": { + "value": "[coalesce(parameters('gallerySolutions'), createArray())[copyIndex()].plan]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.32.4.45862", + "templateHash": "10255889523646649592" + }, + "name": "Operations Management Solutions", + "description": "This module deploys an Operations Management Solution.", + "owner": "Azure/module-maintainers" + }, + "definitions": { + "solutionPlanType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the solution to be created.\nFor solutions authored by Microsoft, the name must be in the pattern: `SolutionType(WorkspaceName)`, for example: `AntiMalware(contoso-Logs)`.\nFor solutions authored by third parties, it can be anything.\nThe solution type is case-sensitive.\nIf not provided, the value of the `name` parameter will be used." + } + }, + "product": { + "type": "string", + "metadata": { + "description": "Required. The product name of the deployed solution.\nFor Microsoft published gallery solution it should be `OMSGallery/{solutionType}`, for example `OMSGallery/AntiMalware`.\nFor a third party solution, it can be anything.\nThis is case sensitive." + } + }, + "publisher": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The publisher name of the deployed solution. For Microsoft published gallery solution, it is `Microsoft`, which is the default value." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the solution.\nFor solutions authored by Microsoft, the name must be in the pattern: `SolutionType(WorkspaceName)`, for example: `AntiMalware(contoso-Logs)`.\nFor solutions authored by third parties, the name should be in the pattern: `SolutionType[WorkspaceName]`, for example `MySolution[contoso-Logs]`.\nThe solution type is case-sensitive." + } + }, + "plan": { + "$ref": "#/definitions/solutionPlanType", + "metadata": { + "description": "Required. Plan for solution object supported by the OperationsManagement resource provider." + } + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "metadata": { + "description": "Required. Name of the Log Analytics workspace where the solution will be deployed/enabled." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.operationsmanagement-solution.{0}.{1}', replace('0.3.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "logAnalyticsWorkspace": { + "existing": true, + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2021-06-01", + "name": "[parameters('logAnalyticsWorkspaceName')]" + }, + "solution": { + "type": "Microsoft.OperationsManagement/solutions", + "apiVersion": "2015-11-01-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "properties": { + "workspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('logAnalyticsWorkspaceName'))]" + }, + "plan": { + "name": "[coalesce(tryGet(parameters('plan'), 'name'), parameters('name'))]", + "promotionCode": "", + "product": "[parameters('plan').product]", + "publisher": "[coalesce(tryGet(parameters('plan'), 'publisher'), 'Microsoft')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed solution." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed solution." + }, + "value": "[resourceId('Microsoft.OperationsManagement/solutions', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group where the solution is deployed." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('solution', '2015-11-01-preview', 'full').location]" + } + } + } + }, + "dependsOn": [ + "logAnalyticsWorkspace" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed log analytics workspace." + }, + "value": "[resourceId('Microsoft.OperationalInsights/workspaces', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed log analytics workspace." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed log analytics workspace." + }, + "value": "[parameters('name')]" + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "metadata": { + "description": "The ID associated with the workspace." + }, + "value": "[reference('logAnalyticsWorkspace').customerId]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('logAnalyticsWorkspace', '2025-02-01', 'full').location]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('logAnalyticsWorkspace', '2025-02-01', 'full'), 'identity'), 'principalId')]" + }, + "primarySharedKey": { + "type": "securestring", + "metadata": { + "description": "The primary shared key of the log analytics workspace." + }, + "value": "[listKeys('logAnalyticsWorkspace', '2025-02-01').primarySharedKey]" + }, + "secondarySharedKey": { + "type": "securestring", + "metadata": { + "description": "The secondary shared key of the log analytics workspace." + }, + "value": "[listKeys('logAnalyticsWorkspace', '2025-02-01').secondarySharedKey]" + } + } + } + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "value": "[reference('logAnalytics').outputs.resourceId.value]" + }, + "name": { + "type": "string", + "value": "[reference('logAnalytics').outputs.name.value]" + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "value": "[reference('logAnalytics').outputs.logAnalyticsWorkspaceId.value]" + }, + "primarySharedKey": { + "type": "securestring", + "value": "[listOutputsWithSecureValues('logAnalytics', '2025-04-01').primarySharedKey]" + }, + "secondarySharedKey": { + "type": "securestring", + "value": "[listOutputsWithSecureValues('logAnalytics', '2025-04-01').secondarySharedKey]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "appInsights": { + "condition": "[variables('shouldCreateAppInsights')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('appInsights-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appInsightsName": { + "value": "[variables('appInsightsName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "logAnalyticsResourceId": "[if(not(empty(parameters('existingLogAnalyticsWorkspaceId'))), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), if(variables('shouldCreateLogAnalytics'), createObject('value', reference('logAnalytics').outputs.resourceId.value), createObject('value', '')))]", + "roleAssignedManagedIdentityPrincipalIds": { + "value": [ + "[reference('userAssignedIdentity').outputs.principalId.value]" + ] + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "7244230154190410370" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources" + } + }, + "appInsightsName": { + "type": "string", + "metadata": { + "description": "Application Insights name" + } + }, + "logAnalyticsResourceId": { + "type": "string", + "metadata": { + "description": "Log Analytics resource id output from log-analytics-ws.bicep module" + } + }, + "roleAssignedManagedIdentityPrincipalIds": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [], + "metadata": { + "description": "Managed Identity that will be given access to the Application Insights" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for resources" + } + } + }, + "resources": { + "applicationInsights": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('applicationInsights-{0}', uniqueString('applicationInsights', deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('appInsightsName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "workspaceResourceId": { + "value": "[parameters('logAnalyticsResourceId')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "disableLocalAuth": { + "value": false + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "5735496719243704506" + }, + "name": "Application Insights", + "description": "This component deploys an Application Insights instance." + }, + "definitions": { + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.3.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.3.0" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Application Insights." + } + }, + "applicationType": { + "type": "string", + "defaultValue": "web", + "allowedValues": [ + "web", + "other" + ], + "metadata": { + "description": "Optional. Application type." + } + }, + "workspaceResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the log analytics workspace which the data will be ingested to. This property is required to create an application with this API version. Applications from older versions will not have this property." + } + }, + "disableIpMasking": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Disable IP masking. Default value is set to true." + } + }, + "disableLocalAuth": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Disable Non-AAD based Auth. Default value is set to false." + } + }, + "forceCustomerStorageForProfiler": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Force users to create their own storage account for profiler and debugger." + } + }, + "linkedStorageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Linked storage account resource ID." + } + }, + "publicNetworkAccessForIngestion": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. The network access type for accessing Application Insights ingestion. - Enabled or Disabled." + } + }, + "publicNetworkAccessForQuery": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. The network access type for accessing Application Insights query. - Enabled or Disabled." + } + }, + "retentionInDays": { + "type": "int", + "defaultValue": 365, + "allowedValues": [ + 30, + 60, + 90, + 120, + 180, + 270, + 365, + 550, + 730 + ], + "metadata": { + "description": "Optional. Retention period in days." + } + }, + "samplingPercentage": { + "type": "int", + "defaultValue": 100, + "minValue": 0, + "maxValue": 100, + "metadata": { + "description": "Optional. Percentage of the data produced by the application being monitored that is being sampled for Application Insights telemetry." + } + }, + "flowType": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Used by the Application Insights system to determine what kind of flow this component was created by. This is to be set to 'Bluefield' when creating/updating a component via the REST API." + } + }, + "requestSource": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Describes what tool created this Application Insights component. Customers using this API should set this to the default 'rest'." + } + }, + "kind": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The kind of application that this component refers to, used to customize UI. This value is a freeform string, values should typically be one of the following: web, ios, other, store, java, phone." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]", + "Monitoring Metrics Publisher": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3913510d-42f4-4e42-8a64-420c390055eb')]", + "Application Insights Component Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ae349356-3a1b-4a5e-921d-050484c6347e')]", + "Application Insights Snapshot Debugger": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '08954f03-6346-4c2e-81c0-ec3a5cfae23b')]", + "Monitoring Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '749f88d5-cbae-40b8-bcfc-e573ddc772fa')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.insights-component.{0}.{1}', replace('0.6.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "appInsights": { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "kind": "[parameters('kind')]", + "properties": { + "Application_Type": "[parameters('applicationType')]", + "DisableIpMasking": "[parameters('disableIpMasking')]", + "DisableLocalAuth": "[parameters('disableLocalAuth')]", + "ForceCustomerStorageForProfiler": "[parameters('forceCustomerStorageForProfiler')]", + "WorkspaceResourceId": "[parameters('workspaceResourceId')]", + "publicNetworkAccessForIngestion": "[parameters('publicNetworkAccessForIngestion')]", + "publicNetworkAccessForQuery": "[parameters('publicNetworkAccessForQuery')]", + "RetentionInDays": "[parameters('retentionInDays')]", + "SamplingPercentage": "[parameters('samplingPercentage')]", + "Flow_Type": "[parameters('flowType')]", + "Request_Source": "[parameters('requestSource')]" + } + }, + "appInsights_roleAssignments": { + "copy": { + "name": "appInsights_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Insights/components/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Insights/components', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "appInsights" + ] + }, + "appInsights_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Insights/components/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "appInsights" + ] + }, + "appInsights_diagnosticSettings": { + "copy": { + "name": "appInsights_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Insights/components/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "appInsights" + ] + }, + "linkedStorageAccount": { + "condition": "[not(empty(parameters('linkedStorageAccountResourceId')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-appInsights-linkedStorageAccount', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appInsightsName": { + "value": "[parameters('name')]" + }, + "storageAccountResourceId": { + "value": "[coalesce(parameters('linkedStorageAccountResourceId'), '')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.33.93.31351", + "templateHash": "10861379689695100897" + }, + "name": "Application Insights Linked Storage Account", + "description": "This component deploys an Application Insights Linked Storage Account." + }, + "parameters": { + "appInsightsName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Application Insights instance. Required if the template is used in a standalone deployment." + } + }, + "storageAccountResourceId": { + "type": "string", + "metadata": { + "description": "Required. Linked storage account resource ID." + } + } + }, + "resources": [ + { + "type": "microsoft.insights/components/linkedStorageAccounts", + "apiVersion": "2020-03-01-preview", + "name": "[format('{0}/{1}', parameters('appInsightsName'), 'ServiceProfiler')]", + "properties": { + "linkedStorageAccount": "[parameters('storageAccountResourceId')]" + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the Linked Storage Account." + }, + "value": "ServiceProfiler" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Linked Storage Account." + }, + "value": "[resourceId('microsoft.insights/components/linkedStorageAccounts', parameters('appInsightsName'), 'ServiceProfiler')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the agent pool was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "appInsights" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the application insights component." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the application insights component." + }, + "value": "[resourceId('Microsoft.Insights/components', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the application insights component was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "applicationId": { + "type": "string", + "metadata": { + "description": "The application ID of the application insights component." + }, + "value": "[reference('appInsights').AppId]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('appInsights', '2020-02-02', 'full').location]" + }, + "instrumentationKey": { + "type": "string", + "metadata": { + "description": "Application Insights Instrumentation key. A read-only value that applications can use to identify the destination for all telemetry sent to Azure Application Insights. This value will be supplied upon construction of each new Application Insights component." + }, + "value": "[reference('appInsights').InstrumentationKey]" + }, + "connectionString": { + "type": "string", + "metadata": { + "description": "Application Insights Connection String." + }, + "value": "[reference('appInsights').ConnectionString]" + } + } + } + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "value": "[reference('applicationInsights').outputs.resourceId.value]" + }, + "applicationId": { + "type": "string", + "value": "[reference('applicationInsights').outputs.applicationId.value]" + }, + "instrumentationKey": { + "type": "string", + "value": "[reference('applicationInsights').outputs.instrumentationKey.value]" + }, + "connectionString": { + "type": "string", + "value": "[reference('applicationInsights').outputs.connectionString.value]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "userAssignedIdentity" + ] + }, + "storage": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('storage-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[variables('storageAccountName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "docsContainerName": { + "value": "[parameters('docsContainerName')]" + }, + "roleAssignedManagedIdentityPrincipalIds": { + "value": [ + "[reference('userAssignedIdentity').outputs.principalId.value]" + ] + }, + "enablePrivateEndpoint": { + "value": "[variables('isAILZIntegrated')]" + }, + "privateEndpointSubnetId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').privateEndpointSubnetId), createObject('value', ''))]", + "blobPrivateDnsZoneId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').privateDnsZoneIds.blob), createObject('value', ''))]", + "publicNetworkAccess": "[if(variables('isAILZIntegrated'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "logAnalyticsWorkspaceId": "[if(not(empty(parameters('existingLogAnalyticsWorkspaceId'))), createObject('value', parameters('existingLogAnalyticsWorkspaceId')), if(variables('shouldCreateLogAnalytics'), createObject('value', reference('logAnalytics').outputs.resourceId.value), createObject('value', '')))]", + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "11526770419544635876" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources" + } + }, + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Storage account name" + } + }, + "skuName": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_RAGRS", + "Standard_ZRS", + "Premium_LRS", + "Premium_ZRS" + ], + "metadata": { + "description": "Storage account SKU name" + } + }, + "docsContainerName": { + "type": "string", + "metadata": { + "description": "Container name for documents" + } + }, + "roleAssignedManagedIdentityPrincipalIds": { + "type": "array", + "metadata": { + "description": "Managed identities that will be given access to the storage account" + } + }, + "enablePrivateEndpoint": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable private endpoint for AILZ mode" + } + }, + "privateEndpointSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Subnet ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "blobPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Blob Private DNS Zone ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "queuePrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Queue Private DNS Zone ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Public network access setting" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Log Analytics Workspace ID for diagnostic settings" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for resources" + } + } + }, + "variables": { + "copy": [ + { + "name": "accountRoleAssignments", + "count": "[length(parameters('roleAssignedManagedIdentityPrincipalIds'))]", + "input": { + "principalId": "[parameters('roleAssignedManagedIdentityPrincipalIds')[copyIndex('accountRoleAssignments')]]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "Contributor" + } + }, + { + "name": "blobRoleAssignments", + "count": "[length(parameters('roleAssignedManagedIdentityPrincipalIds'))]", + "input": { + "principalId": "[parameters('roleAssignedManagedIdentityPrincipalIds')[copyIndex('blobRoleAssignments')]]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "Storage Blob Data Contributor" + } + }, + { + "name": "queueRoleAssignments", + "count": "[length(parameters('roleAssignedManagedIdentityPrincipalIds'))]", + "input": { + "principalId": "[parameters('roleAssignedManagedIdentityPrincipalIds')[copyIndex('queueRoleAssignments')]]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "Storage Queue Data Contributor" + } + } + ], + "deployerRoleAssignments": [ + { + "principalId": "[deployer().objectId]", + "principalType": "[if(empty(coalesce(tryGet(deployer(), 'userPrincipalName'), '')), 'ServicePrincipal', 'User')]", + "roleDefinitionIdOrName": "Storage Blob Data Contributor" + }, + { + "principalId": "[deployer().objectId]", + "principalType": "[if(empty(coalesce(tryGet(deployer(), 'userPrincipalName'), '')), 'ServicePrincipal', 'User')]", + "roleDefinitionIdOrName": "Storage Queue Data Contributor" + } + ] + }, + "resources": { + "storageAccount": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}.storageAccount', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('storageAccountName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "kind": { + "value": "StorageV2" + }, + "skuName": { + "value": "[parameters('skuName')]" + }, + "accessTier": { + "value": "Hot" + }, + "diagnosticSettings": { + "value": [ + { + "workspaceResourceId": "[parameters('logAnalyticsWorkspaceId')]" + } + ] + }, + "allowSharedKeyAccess": { + "value": false + }, + "enableHierarchicalNamespace": { + "value": false + }, + "publicNetworkAccess": { + "value": "[parameters('publicNetworkAccess')]" + }, + "networkAcls": "[if(parameters('enablePrivateEndpoint'), createObject('value', createObject('defaultAction', 'Deny', 'bypass', 'AzureServices', 'ipRules', createArray(), 'virtualNetworkRules', createArray())), createObject('value', createObject('defaultAction', 'Allow', 'bypass', 'AzureServices')))]", + "privateEndpoints": "[if(parameters('enablePrivateEndpoint'), createObject('value', createArray(createObject('name', format('{0}-blob-pe', parameters('storageAccountName')), 'resourceGroupResourceId', resourceGroup().id, 'subnetResourceId', parameters('privateEndpointSubnetId'), 'service', 'blob', 'privateLinkServiceConnectionName', format('{0}-blob-plsc', parameters('storageAccountName')), 'privateDnsZoneGroups', createArray(createObject('name', 'blob-dns-zone-group', 'privateDnsZoneGroupConfigs', createArray(createObject('name', 'blob-config', 'privateDnsZoneResourceId', parameters('blobPrivateDnsZoneId')))))), createObject('name', format('{0}-queue-pe', parameters('storageAccountName')), 'resourceGroupResourceId', resourceGroup().id, 'subnetResourceId', parameters('privateEndpointSubnetId'), 'service', 'queue', 'privateLinkServiceConnectionName', format('{0}-queue-plsc', parameters('storageAccountName')), 'privateDnsZoneGroups', createArray(createObject('name', 'queue-dns-zone-group', 'privateDnsZoneGroupConfigs', createArray(createObject('name', 'queue-config', 'privateDnsZoneResourceId', parameters('queuePrivateDnsZoneId')))))))), createObject('value', createArray()))]", + "blobServices": { + "value": { + "automaticSnapshotPolicyEnabled": true, + "deleteRetentionPolicyDays": 7, + "deleteRetentionPolicyEnabled": true, + "containerDeleteRetentionPolicyDays": 7, + "containerDeleteRetentionPolicyEnabled": true, + "containers": [ + { + "name": "[parameters('docsContainerName')]", + "publicAccess": "None" + } + ] + } + }, + "queueServices": { + "value": { + "queues": [ + { + "name": "contentflow-execution-requests", + "metadata": {} + } + ] + } + }, + "roleAssignments": { + "value": "[concat(variables('accountRoleAssignments'), variables('blobRoleAssignments'), variables('queueRoleAssignments'), variables('deployerRoleAssignments'))]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "3530476863855541150" + }, + "name": "Storage Accounts", + "description": "This module deploys a Storage Account." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the private endpoints output." + } + }, + "networkAclsType": { + "type": "object", + "properties": { + "resourceAccessRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tenantId": { + "type": "string", + "metadata": { + "description": "Required. The ID of the tenant in which the resource resides in." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the target service. Can also contain a wildcard, if multiple services e.g. in a resource group should be included." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Sets the resource access rules. Array entries must consist of \"tenantId\" and \"resourceId\" fields only." + } + }, + "bypass": { + "type": "string", + "allowedValues": [ + "AzureServices", + "AzureServices, Logging", + "AzureServices, Logging, Metrics", + "AzureServices, Metrics", + "Logging", + "Logging, Metrics", + "Metrics", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether traffic is bypassed for Logging/Metrics/AzureServices. Possible values are any combination of Logging,Metrics,AzureServices (For example, \"Logging, Metrics\"), or None to bypass none of those traffics." + } + }, + "virtualNetworkRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Sets the virtual network rules." + } + }, + "ipRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Sets the IP ACL rules." + } + }, + "defaultAction": { + "type": "string", + "allowedValues": [ + "Allow", + "Deny" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the default action of allow or deny when no other rules match." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the network configuration." + } + }, + "secretsExportConfigurationType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The key vault name where to store the keys and connection strings generated by the modules." + } + }, + "accessKey1Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The accessKey1 secret name to create." + } + }, + "connectionString1Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The connectionString1 secret name to create." + } + }, + "accessKey2Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The accessKey2 secret name to create." + } + }, + "connectionString2Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The connectionString2 secret name to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of the exported secrets." + } + }, + "localUserType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the local user used for SFTP Authentication." + } + }, + "hasSharedKey": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether shared key exists. Set it to false to remove existing shared key." + } + }, + "hasSshKey": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH key exists. Set it to false to remove existing SSH key." + } + }, + "hasSshPassword": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH password exists. Set it to false to remove existing SSH password." + } + }, + "homeDirectory": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The local user home directory." + } + }, + "permissionScopes": { + "type": "array", + "items": { + "$ref": "#/definitions/permissionScopeType" + }, + "metadata": { + "description": "Required. The permission scopes of the local user." + } + }, + "sshAuthorizedKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/sshAuthorizedKeyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The local user SSH authorized keys for SFTP." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a local user." + } + }, + "blobServiceType": { + "type": "object", + "properties": { + "automaticSnapshotPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Automatic Snapshot is enabled if set to true." + } + }, + "changeFeedEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for change feed events. Indicates whether change feed event logging is enabled for the Blob service." + } + }, + "changeFeedRetentionInDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 146000, + "metadata": { + "description": "Optional. Indicates whether change feed event logging is enabled for the Blob service. Indicates the duration of changeFeed retention in days. If left blank, it indicates an infinite retention of the change feed." + } + }, + "containerDeleteRetentionPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for container soft delete. Indicates whether DeleteRetentionPolicy is enabled." + } + }, + "containerDeleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted item should be retained." + } + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "defaultServiceVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Indicates the default version to use for requests to the Blob service if an incoming request's version is not specified. Possible values include version 2008-10-27 and all more recent versions." + } + }, + "deleteRetentionPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for blob soft delete." + } + }, + "deleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted blob should be retained." + } + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "isVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Use versioning to automatically maintain previous versions of your blobs. Cannot be enabled for ADLS Gen2 storage accounts." + } + }, + "lastAccessTimeTrackingPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service property to configure last access time based tracking policy. When set to true last access time based tracking is enabled." + } + }, + "restorePolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for blob restore policy. If point-in-time restore is enabled, then versioning, change feed, and blob soft delete must also be enabled." + } + }, + "restorePolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "metadata": { + "description": "Optional. How long this blob can be restored. It should be less than DeleteRetentionPolicy days." + } + }, + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/containerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Blob containers to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a blob service." + } + }, + "_1.immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "metadata": { + "description": "The type for an immutability policy.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/container/main.bicep" + } + } + }, + "_2.secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "_3.lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_3.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_3.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_3.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_3.roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "containerType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/_1.immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "nullable": true + }, + "publicAccess": { + "type": "string", + "allowedValues": [ + "Blob", + "Container", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "description": "The type of a storage container.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/main.bicep" + } + } + }, + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "description": "The type for a cors rule.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/main.bicep" + } + } + }, + "customerManagedKeyWithAutoRotateType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + } + }, + "keyName": { + "type": "string", + "metadata": { + "description": "Required. The name of the customer managed key to use for encryption." + } + }, + "keyVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, using version as per 'autoRotationEnabled' setting." + } + }, + "autoRotationEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable auto-rotating to the latest key version. Default is `true`. If set to `false`, the latest key version at the time of the deployment is used." + } + }, + "userAssignedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type supports auto-rotation of the customer-managed key.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingMetricsOnlyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of diagnostic setting." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "permissionScopeType": { + "type": "object", + "properties": { + "permissions": { + "type": "string", + "metadata": { + "description": "Required. The permissions for the local user. Possible values include: Read (r), Write (w), Delete (d), List (l), and Create (c)." + } + }, + "resourceName": { + "type": "string", + "metadata": { + "description": "Required. The name of resource, normally the container name or the file share name, used by the local user." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The service used by the local user, e.g. blob, file." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "local-user/main.bicep" + } + } + }, + "privateEndpointMultiServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the private endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The subresource to deploy the private endpoint for. For example \"blob\", \"table\", \"queue\" or \"file\" for a Storage Account's Private Endpoints." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_3.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "lock": { + "$ref": "#/definitions/_3.lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can NOT be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "secretsOutputType": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/definitions/_2.secretSetOutputType", + "metadata": { + "description": "An exported secret's references." + } + }, + "metadata": { + "description": "A map of the exported secrets", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "sshAuthorizedKeyType": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Description used to store the function/usage of the key." + } + }, + "key": { + "type": "securestring", + "metadata": { + "description": "Required. SSH public key base64 encoded. The format should be: '{keyType} {keyData}', e.g. ssh-rsa AAAABBBB." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "local-user/main.bicep" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Required. Name of the Storage Account. Must be lower-case." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "kind": { + "type": "string", + "defaultValue": "StorageV2", + "allowedValues": [ + "Storage", + "StorageV2", + "BlobStorage", + "FileStorage", + "BlockBlobStorage" + ], + "metadata": { + "description": "Optional. Type of Storage Account to create." + } + }, + "skuName": { + "type": "string", + "defaultValue": "Standard_GRS", + "allowedValues": [ + "Standard_LRS", + "Standard_ZRS", + "Standard_GRS", + "Standard_GZRS", + "Standard_RAGRS", + "Standard_RAGZRS", + "StandardV2_LRS", + "StandardV2_ZRS", + "StandardV2_GRS", + "StandardV2_GZRS", + "Premium_LRS", + "Premium_ZRS", + "PremiumV2_LRS", + "PremiumV2_ZRS" + ], + "metadata": { + "description": "Optional. Storage Account Sku Name - note: certain V2 SKUs require the use of: kind = FileStorage." + } + }, + "accessTier": { + "type": "string", + "defaultValue": "Hot", + "allowedValues": [ + "Premium", + "Hot", + "Cool", + "Cold" + ], + "metadata": { + "description": "Conditional. Required if the Storage Account kind is set to BlobStorage. The access tier is used for billing. The \"Premium\" access tier is the default value for premium block blobs storage account type and it cannot be changed for the premium block blobs storage account type." + } + }, + "largeFileSharesState": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Allow large file shares if set to 'Enabled'. It cannot be disabled once it is enabled. Only supported on locally redundant and zone redundant file shares. It cannot be set on FileStorage storage accounts (storage accounts for premium file shares)." + } + }, + "azureFilesIdentityBasedAuthentication": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts@2024-01-01#properties/properties/properties/azureFilesIdentityBasedAuthentication" + }, + "description": "Optional. Provides the identity based authentication settings for Azure Files." + }, + "nullable": true + }, + "defaultToOAuthAuthentication": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. A boolean flag which indicates whether the default authentication is OAuth or not." + } + }, + "allowSharedKeyAccess": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether the storage account permits requests to be authorized with the account access key via Shared Key. If false, then all requests, including shared access signatures, must be authorized with Azure Active Directory (Azure AD). The default value is null, which is equivalent to true." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointMultiServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." + } + }, + "managementPolicyRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. The Storage Account ManagementPolicies Rules." + } + }, + "networkAcls": { + "$ref": "#/definitions/networkAclsType", + "nullable": true, + "metadata": { + "description": "Optional. Networks ACLs, this value contains IPs to whitelist and/or Subnet information. If in use, bypass needs to be supplied. For security reasons, it is recommended to set the DefaultAction Deny." + } + }, + "requireInfrastructureEncryption": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. A Boolean indicating whether or not the service applies a secondary layer of encryption with platform managed keys for data at rest. For security reasons, it is recommended to set it to true." + } + }, + "allowCrossTenantReplication": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Allow or disallow cross AAD tenant object replication." + } + }, + "customDomainName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Sets the custom domain name assigned to the storage account. Name is the CNAME source." + } + }, + "customDomainUseSubDomainName": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether indirect CName validation is enabled. This should only be set on updates." + } + }, + "dnsEndpointType": { + "type": "string", + "nullable": true, + "allowedValues": [ + "AzureDnsZone", + "Standard" + ], + "metadata": { + "description": "Optional. Allows you to specify the type of endpoint. Set this to AzureDNSZone to create a large number of accounts in a single subscription, which creates accounts in an Azure DNS Zone and the endpoint URL will have an alphanumeric DNS Zone identifier." + } + }, + "blobServices": { + "$ref": "#/definitions/blobServiceType", + "defaultValue": "[if(not(equals(parameters('kind'), 'FileStorage')), createObject('containerDeleteRetentionPolicyEnabled', true(), 'containerDeleteRetentionPolicyDays', 7, 'deleteRetentionPolicyEnabled', true(), 'deleteRetentionPolicyDays', 6), createObject())]", + "metadata": { + "description": "Optional. Blob service and containers to deploy." + } + }, + "fileServices": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. File service and shares to deploy." + } + }, + "queueServices": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Queue service and queues to create." + } + }, + "tableServices": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Table service and tables to create." + } + }, + "allowBlobPublicAccess": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether public access is enabled for all blobs or containers in the storage account. For security reasons, it is recommended to set it to false." + } + }, + "minimumTlsVersion": { + "type": "string", + "defaultValue": "TLS1_2", + "allowedValues": [ + "TLS1_2" + ], + "metadata": { + "description": "Optional. Set the minimum TLS version on request to storage. The TLS versions 1.0 and 1.1 are deprecated and not supported anymore." + } + }, + "enableHierarchicalNamespace": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Conditional. If true, enables Hierarchical Namespace for the storage account. Required if enableSftp or enableNfsV3 is set to true." + } + }, + "enableSftp": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. If true, enables Secure File Transfer Protocol for the storage account. Requires enableHierarchicalNamespace to be true." + } + }, + "localUsers": { + "type": "array", + "items": { + "$ref": "#/definitions/localUserType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Local users to deploy for SFTP authentication." + } + }, + "isLocalUserEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables local users feature, if set to true." + } + }, + "enableNfsV3": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. If true, enables NFS 3.0 support for the storage account. Requires enableHierarchicalNamespace to be true." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts@2024-01-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "allowedCopyScope": { + "type": "string", + "nullable": true, + "allowedValues": [ + "AAD", + "PrivateLink" + ], + "metadata": { + "description": "Optional. Restrict copy to and from Storage Accounts within an AAD tenant or with Private Links to the same VNet." + } + }, + "publicNetworkAccess": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." + } + }, + "supportsHttpsTrafficOnly": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Allows HTTPS traffic only to storage service if sets to true." + } + }, + "customerManagedKey": { + "$ref": "#/definitions/customerManagedKeyWithAutoRotateType", + "nullable": true, + "metadata": { + "description": "Optional. The customer managed key definition." + } + }, + "sasExpirationPeriod": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The SAS expiration period. DD.HH:MM:SS." + } + }, + "sasExpirationAction": { + "type": "string", + "defaultValue": "Log", + "allowedValues": [ + "Block", + "Log" + ], + "metadata": { + "description": "Optional. The SAS expiration action. Allowed values are Block and Log." + } + }, + "keyType": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Account", + "Service" + ], + "metadata": { + "description": "Optional. The keyType to use with Queue & Table services." + } + }, + "secretsExportConfiguration": { + "$ref": "#/definitions/secretsExportConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Key vault reference and secret settings for the module's secrets export." + } + }, + "immutableStorageWithVersioning": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts@2025-01-01#properties/properties/properties/immutableStorageWithVersioning" + }, + "description": "Optional. The property is immutable and can only be set to true at the account creation time. When set to true, it enables object level immutability for all the new containers in the account by default. Cannot be enabled for ADLS Gen2 storage accounts." + }, + "nullable": true + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "immutabilityValidation": "[if(and(equals(parameters('enableHierarchicalNamespace'), true()), not(empty(parameters('immutableStorageWithVersioning')))), fail('Configuration error: Immutable storage with versioning cannot be enabled when hierarchical namespace is enabled.'), null())]", + "supportsBlobService": "[or(or(or(equals(parameters('kind'), 'BlockBlobStorage'), equals(parameters('kind'), 'BlobStorage')), equals(parameters('kind'), 'StorageV2')), equals(parameters('kind'), 'Storage'))]", + "supportsFileService": "[or(or(equals(parameters('kind'), 'FileStorage'), equals(parameters('kind'), 'StorageV2')), equals(parameters('kind'), 'Storage'))]", + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Blob Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "Storage Blob Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "Storage Blob Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", + "Storage Blob Delegator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a')]", + "Storage File Data Privileged Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '69566ab7-960f-475b-8e7c-b3118f30c6bd')]", + "Storage File Data Privileged Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b8eda974-7b85-4f76-af95-65846b26df6d')]", + "Storage File Data SMB Share Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')]", + "Storage File Data SMB Share Elevated Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7264617-510b-434b-a828-9731dc254ea7')]", + "Storage File Data SMB Share Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'aba4ae5f-2193-4029-9191-0cb91df5e314')]", + "Storage Queue Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "Storage Queue Data Message Processor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')]", + "Storage Queue Data Message Sender": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]", + "Storage Queue Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '19e7f393-937e-4f77-808e-94535e297925')]", + "Storage Table Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "Storage Table Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76199698-9eea-4c19-bc75-cec21354c6b6')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "cMKKeyVault::cMKKey": { + "condition": "[and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), not(empty(tryGet(parameters('customerManagedKey'), 'keyName')))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.storage-storageaccount.{0}.{1}', replace('0.27.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "cMKKeyVault": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId')))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + }, + "cMKUserAssignedIdentity": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + }, + "storageAccount": { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "kind": "[parameters('kind')]", + "sku": { + "name": "[parameters('skuName')]" + }, + "identity": "[variables('identity')]", + "tags": "[parameters('tags')]", + "properties": "[shallowMerge(createArray(createObject('allowSharedKeyAccess', parameters('allowSharedKeyAccess'), 'defaultToOAuthAuthentication', parameters('defaultToOAuthAuthentication'), 'allowCrossTenantReplication', parameters('allowCrossTenantReplication'), 'allowedCopyScope', parameters('allowedCopyScope'), 'customDomain', createObject('name', parameters('customDomainName'), 'useSubDomainName', parameters('customDomainUseSubDomainName')), 'dnsEndpointType', parameters('dnsEndpointType'), 'isLocalUserEnabled', parameters('isLocalUserEnabled'), 'encryption', union(createObject('keySource', if(not(empty(parameters('customerManagedKey'))), 'Microsoft.Keyvault', 'Microsoft.Storage'), 'services', createObject('blob', if(variables('supportsBlobService'), createObject('enabled', true()), null()), 'file', if(variables('supportsFileService'), createObject('enabled', true()), null()), 'table', createObject('enabled', true(), 'keyType', parameters('keyType')), 'queue', createObject('enabled', true(), 'keyType', parameters('keyType'))), 'keyvaultproperties', if(not(empty(parameters('customerManagedKey'))), createObject('keyname', parameters('customerManagedKey').keyName, 'keyvaulturi', reference('cMKKeyVault').vaultUri, 'keyversion', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), parameters('customerManagedKey').keyVersion, if(coalesce(tryGet(parameters('customerManagedKey'), 'autoRotationEnabled'), true()), null(), last(split(reference('cMKKeyVault::cMKKey').keyUriWithVersion, '/'))))), null()), 'identity', createObject('userAssignedIdentity', if(not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'))), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2], split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]), 'Microsoft.ManagedIdentity/userAssignedIdentities', last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))), null()))), if(parameters('requireInfrastructureEncryption'), createObject('requireInfrastructureEncryption', if(not(equals(parameters('kind'), 'Storage')), parameters('requireInfrastructureEncryption'), null())), createObject())), 'accessTier', if(and(not(equals(parameters('kind'), 'Storage')), not(equals(parameters('kind'), 'BlockBlobStorage'))), parameters('accessTier'), null()), 'sasPolicy', if(not(empty(parameters('sasExpirationPeriod'))), createObject('expirationAction', parameters('sasExpirationAction'), 'sasExpirationPeriod', parameters('sasExpirationPeriod')), null()), 'supportsHttpsTrafficOnly', parameters('supportsHttpsTrafficOnly'), 'isSftpEnabled', parameters('enableSftp'), 'isNfsV3Enabled', if(parameters('enableNfsV3'), parameters('enableNfsV3'), ''), 'largeFileSharesState', if(or(equals(parameters('skuName'), 'Standard_LRS'), equals(parameters('skuName'), 'Standard_ZRS')), parameters('largeFileSharesState'), null()), 'minimumTlsVersion', parameters('minimumTlsVersion'), 'networkAcls', if(not(empty(parameters('networkAcls'))), union(createObject('resourceAccessRules', tryGet(parameters('networkAcls'), 'resourceAccessRules'), 'defaultAction', coalesce(tryGet(parameters('networkAcls'), 'defaultAction'), 'Deny'), 'virtualNetworkRules', tryGet(parameters('networkAcls'), 'virtualNetworkRules'), 'ipRules', tryGet(parameters('networkAcls'), 'ipRules')), if(contains(parameters('networkAcls'), 'bypass'), createObject('bypass', tryGet(parameters('networkAcls'), 'bypass')), createObject())), createObject('bypass', 'AzureServices', 'defaultAction', 'Deny')), 'allowBlobPublicAccess', parameters('allowBlobPublicAccess'), 'publicNetworkAccess', if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(and(not(empty(parameters('privateEndpoints'))), empty(parameters('networkAcls'))), 'Disabled', null()))), if(not(empty(parameters('azureFilesIdentityBasedAuthentication'))), createObject('azureFilesIdentityBasedAuthentication', parameters('azureFilesIdentityBasedAuthentication')), createObject()), if(not(equals(parameters('enableHierarchicalNamespace'), null())), createObject('isHnsEnabled', parameters('enableHierarchicalNamespace')), createObject()), createObject('immutableStorageWithVersioning', parameters('immutableStorageWithVersioning'))))]", + "dependsOn": [ + "cMKKeyVault", + "cMKKeyVault::cMKKey" + ] + }, + "storageAccount_diagnosticSettings": { + "copy": { + "name": "storageAccount_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_roleAssignments": { + "copy": { + "name": "storageAccount_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_privateEndpoints": { + "copy": { + "name": "storageAccount_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sa-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Storage/storageAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Storage/storageAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_managementPolicies": { + "condition": "[not(empty(coalesce(parameters('managementPolicyRules'), createArray())))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-ManagementPolicies', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "rules": { + "value": "[parameters('managementPolicyRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "14529265638306912023" + }, + "name": "Storage Account Management Policies", + "description": "This module deploys a Storage Account Management Policy." + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "rules": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/managementPolicies@2024-01-01#properties/properties/properties/policy/properties/rules" + }, + "description": "Required. The Storage Account ManagementPolicies Rules." + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/managementPolicies", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]", + "properties": { + "policy": { + "rules": "[parameters('rules')]" + } + } + } + ], + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed management policy." + }, + "value": "default" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed management policy." + }, + "value": "default" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed management policy." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount", + "storageAccount_blobServices" + ] + }, + "storageAccount_localUsers": { + "copy": { + "name": "storageAccount_localUsers", + "count": "[length(coalesce(parameters('localUsers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-LocalUsers-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].name]" + }, + "hasSshKey": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].hasSshKey]" + }, + "hasSshPassword": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].hasSshPassword]" + }, + "permissionScopes": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].permissionScopes]" + }, + "hasSharedKey": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'hasSharedKey')]" + }, + "homeDirectory": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'homeDirectory')]" + }, + "sshAuthorizedKeys": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'sshAuthorizedKeys')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "3261275799710495788" + }, + "name": "Storage Account Local Users", + "description": "This module deploys a Storage Account Local User, which is used for SFTP authentication." + }, + "definitions": { + "sshAuthorizedKeyType": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Description used to store the function/usage of the key." + } + }, + "key": { + "type": "securestring", + "metadata": { + "description": "Required. SSH public key base64 encoded. The format should be: '{keyType} {keyData}', e.g. ssh-rsa AAAABBBB." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "permissionScopeType": { + "type": "object", + "properties": { + "permissions": { + "type": "string", + "metadata": { + "description": "Required. The permissions for the local user. Possible values include: Read (r), Write (w), Delete (d), List (l), and Create (c)." + } + }, + "resourceName": { + "type": "string", + "metadata": { + "description": "Required. The name of resource, normally the container name or the file share name, used by the local user." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The service used by the local user, e.g. blob, file." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the local user used for SFTP Authentication." + } + }, + "hasSharedKey": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether shared key exists. Set it to false to remove existing shared key." + } + }, + "hasSshKey": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH key exists. Set it to false to remove existing SSH key." + } + }, + "hasSshPassword": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH password exists. Set it to false to remove existing SSH password." + } + }, + "homeDirectory": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The local user home directory." + } + }, + "permissionScopes": { + "type": "array", + "items": { + "$ref": "#/definitions/permissionScopeType" + }, + "metadata": { + "description": "Required. The permission scopes of the local user." + } + }, + "sshAuthorizedKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/sshAuthorizedKeyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The local user SSH authorized keys for SFTP." + } + } + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "localUsers": { + "type": "Microsoft.Storage/storageAccounts/localUsers", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "properties": { + "hasSharedKey": "[parameters('hasSharedKey')]", + "hasSshKey": "[parameters('hasSshKey')]", + "hasSshPassword": "[parameters('hasSshPassword')]", + "homeDirectory": "[parameters('homeDirectory')]", + "permissionScopes": "[parameters('permissionScopes')]", + "sshAuthorizedKeys": "[parameters('sshAuthorizedKeys')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed local user." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed local user." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed local user." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/localUsers', parameters('storageAccountName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_blobServices": { + "condition": "[not(empty(parameters('blobServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-BlobServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "containers": { + "value": "[tryGet(parameters('blobServices'), 'containers')]" + }, + "automaticSnapshotPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'automaticSnapshotPolicyEnabled')]" + }, + "changeFeedEnabled": { + "value": "[tryGet(parameters('blobServices'), 'changeFeedEnabled')]" + }, + "changeFeedRetentionInDays": { + "value": "[tryGet(parameters('blobServices'), 'changeFeedRetentionInDays')]" + }, + "containerDeleteRetentionPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyEnabled')]" + }, + "containerDeleteRetentionPolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyDays')]" + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyAllowPermanentDelete')]" + }, + "corsRules": { + "value": "[tryGet(parameters('blobServices'), 'corsRules')]" + }, + "defaultServiceVersion": { + "value": "[tryGet(parameters('blobServices'), 'defaultServiceVersion')]" + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyAllowPermanentDelete')]" + }, + "deleteRetentionPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyEnabled')]" + }, + "deleteRetentionPolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyDays')]" + }, + "isVersioningEnabled": { + "value": "[tryGet(parameters('blobServices'), 'isVersioningEnabled')]" + }, + "lastAccessTimeTrackingPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'lastAccessTimeTrackingPolicyEnabled')]" + }, + "restorePolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'restorePolicyEnabled')]" + }, + "restorePolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'restorePolicyDays')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('blobServices'), 'diagnosticSettings')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9925173705553594819" + }, + "name": "Storage Account blob Services", + "description": "This module deploys a Storage Account Blob Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "containerType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "nullable": true + }, + "publicAccess": { + "type": "string", + "allowedValues": [ + "Blob", + "Container", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a storage container." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "metadata": { + "description": "The type for an immutability policy.", + "__bicep_imported_from!": { + "sourceTemplate": "container/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "automaticSnapshotPolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Automatic Snapshot is enabled if set to true." + } + }, + "changeFeedEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service properties for change feed events. Indicates whether change feed event logging is enabled for the Blob service." + } + }, + "changeFeedRetentionInDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 146000, + "metadata": { + "description": "Optional. Indicates whether change feed event logging is enabled for the Blob service. Indicates the duration of changeFeed retention in days. If left blank, it indicates an infinite retention of the change feed." + } + }, + "containerDeleteRetentionPolicyEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The blob service properties for container soft delete. Indicates whether DeleteRetentionPolicy is enabled." + } + }, + "containerDeleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted item should be retained." + } + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "defaultServiceVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Indicates the default version to use for requests to the Blob service if an incoming request's version is not specified. Possible values include version 2008-10-27 and all more recent versions." + } + }, + "deleteRetentionPolicyEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The blob service properties for blob soft delete." + } + }, + "deleteRetentionPolicyDays": { + "type": "int", + "defaultValue": 7, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted blob should be retained." + } + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "isVersioningEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Use versioning to automatically maintain previous versions of your blobs. Cannot be enabled for ADLS Gen2 storage accounts." + } + }, + "lastAccessTimeTrackingPolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service property to configure last access time based tracking policy. When set to true last access time based tracking is enabled." + } + }, + "restorePolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service properties for blob restore policy. If point-in-time restore is enabled, then versioning, change feed, and blob soft delete must also be enabled." + } + }, + "restorePolicyDays": { + "type": "int", + "defaultValue": 7, + "minValue": 1, + "metadata": { + "description": "Optional. How long this blob can be restored. It should be less than DeleteRetentionPolicy days." + } + }, + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/containerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Blob containers to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "enableReferencedModulesTelemetry": false, + "name": "default" + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "blobServices": { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "properties": { + "automaticSnapshotPolicyEnabled": "[parameters('automaticSnapshotPolicyEnabled')]", + "changeFeed": "[if(parameters('changeFeedEnabled'), createObject('enabled', true(), 'retentionInDays', parameters('changeFeedRetentionInDays')), null())]", + "containerDeleteRetentionPolicy": { + "enabled": "[parameters('containerDeleteRetentionPolicyEnabled')]", + "days": "[parameters('containerDeleteRetentionPolicyDays')]", + "allowPermanentDelete": "[if(equals(parameters('containerDeleteRetentionPolicyEnabled'), true()), parameters('containerDeleteRetentionPolicyAllowPermanentDelete'), null())]" + }, + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]", + "defaultServiceVersion": "[parameters('defaultServiceVersion')]", + "deleteRetentionPolicy": { + "enabled": "[parameters('deleteRetentionPolicyEnabled')]", + "days": "[parameters('deleteRetentionPolicyDays')]", + "allowPermanentDelete": "[if(and(parameters('deleteRetentionPolicyEnabled'), parameters('deleteRetentionPolicyAllowPermanentDelete')), true(), null())]" + }, + "isVersioningEnabled": "[parameters('isVersioningEnabled')]", + "lastAccessTimeTrackingPolicy": "[if(not(equals(reference('storageAccount', '2025-01-01', 'full').kind, 'Storage')), createObject('enable', parameters('lastAccessTimeTrackingPolicyEnabled'), 'name', if(equals(parameters('lastAccessTimeTrackingPolicyEnabled'), true()), 'AccessTimeTracking', null()), 'trackingGranularityInDays', if(equals(parameters('lastAccessTimeTrackingPolicyEnabled'), true()), 1, null())), null())]", + "restorePolicy": "[if(parameters('restorePolicyEnabled'), createObject('enabled', true(), 'days', parameters('restorePolicyDays')), null())]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "blobServices_diagnosticSettings": { + "copy": { + "name": "blobServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "blobServices" + ] + }, + "blobServices_container": { + "copy": { + "name": "blobServices_container", + "count": "[length(coalesce(parameters('containers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Container-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "blobServiceName": { + "value": "[variables('name')]" + }, + "name": { + "value": "[coalesce(parameters('containers'), createArray())[copyIndex()].name]" + }, + "defaultEncryptionScope": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'defaultEncryptionScope')]" + }, + "denyEncryptionScopeOverride": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'denyEncryptionScopeOverride')]" + }, + "enableNfsV3AllSquash": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'enableNfsV3AllSquash')]" + }, + "enableNfsV3RootSquash": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'enableNfsV3RootSquash')]" + }, + "immutableStorageWithVersioningEnabled": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'immutableStorageWithVersioningEnabled')]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'metadata')]" + }, + "publicAccess": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'publicAccess')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "immutabilityPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'immutabilityPolicy')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "5026123498979497429" + }, + "name": "Storage Account Blob Containers", + "description": "This module deploys a Storage Account Blob Container." + }, + "definitions": { + "immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an immutability policy." + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "blobServiceName": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the parent Blob Service. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "defaultValue": {} + }, + "publicAccess": { + "type": "string", + "defaultValue": "None", + "allowedValues": [ + "Container", + "Blob", + "None" + ], + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Blob Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "Storage Blob Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "Storage Blob Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", + "Storage Blob Delegator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::blobServices": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('blobServiceName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.storage-blobcontainer.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "container": { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]", + "properties": { + "defaultEncryptionScope": "[parameters('defaultEncryptionScope')]", + "denyEncryptionScopeOverride": "[parameters('denyEncryptionScopeOverride')]", + "enableNfsV3AllSquash": "[if(equals(parameters('enableNfsV3AllSquash'), true()), parameters('enableNfsV3AllSquash'), null())]", + "enableNfsV3RootSquash": "[if(equals(parameters('enableNfsV3RootSquash'), true()), parameters('enableNfsV3RootSquash'), null())]", + "immutableStorageWithVersioning": "[if(parameters('immutableStorageWithVersioningEnabled'), createObject('enabled', parameters('immutableStorageWithVersioningEnabled')), null())]", + "metadata": "[parameters('metadata')]", + "publicAccess": "[parameters('publicAccess')]" + } + }, + "container_roleAssignments": { + "copy": { + "name": "container_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "container" + ] + }, + "container_immutabilityPolicy": { + "condition": "[not(empty(coalesce(parameters('immutabilityPolicy'), createObject())))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[take(format('{0}-ImmutPol', deployment().name), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "containerName": { + "value": "[parameters('name')]" + }, + "immutabilityPeriodSinceCreationInDays": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'immutabilityPeriodSinceCreationInDays')]" + }, + "allowProtectedAppendWrites": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'allowProtectedAppendWrites')]" + }, + "allowProtectedAppendWritesAll": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'allowProtectedAppendWritesAll')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10782942397325758470" + }, + "name": "Storage Account Blob Container Immutability Policies", + "description": "This module deploys a Storage Account Blob Container Immutability Policy." + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "containerName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent container to apply the policy to. Required if the template is used in a standalone deployment." + } + }, + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "defaultValue": 365, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "variables": { + "name": "default" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}/{2}/{3}', parameters('storageAccountName'), 'default', parameters('containerName'), variables('name'))]", + "properties": { + "immutabilityPeriodSinceCreationInDays": "[parameters('immutabilityPeriodSinceCreationInDays')]", + "allowProtectedAppendWrites": "[parameters('allowProtectedAppendWrites')]", + "allowProtectedAppendWritesAll": "[parameters('allowProtectedAppendWritesAll')]" + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed immutability policy." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed immutability policy." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies', parameters('storageAccountName'), 'default', parameters('containerName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed immutability policy." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "container" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed container." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed container." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed container." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "blobServices" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed blob service." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed blob service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storageAccountName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the deployed blob service." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_fileServices": { + "condition": "[not(empty(parameters('fileServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-FileServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('fileServices'), 'diagnosticSettings')]" + }, + "protocolSettings": { + "value": "[tryGet(parameters('fileServices'), 'protocolSettings')]" + }, + "shareDeleteRetentionPolicy": { + "value": "[tryGet(parameters('fileServices'), 'shareDeleteRetentionPolicy')]" + }, + "shares": { + "value": "[tryGet(parameters('fileServices'), 'shares')]" + }, + "corsRules": { + "value": "[tryGet(parameters('queueServices'), 'corsRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "2735186993322606805" + }, + "name": "Storage Account File Share Services", + "description": "This module deploys a Storage Account File Share Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the file service." + } + }, + "protocolSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/protocolSettings" + }, + "description": "Optional. Protocol settings for file service." + }, + "defaultValue": {} + }, + "shareDeleteRetentionPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/shareDeleteRetentionPolicy" + }, + "description": "Optional. The service properties for soft delete." + }, + "defaultValue": { + "enabled": true, + "days": 7 + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "shares": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. File shares to create." + } + } + }, + "variables": { + "enableReferencedModulesTelemetry": false + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "fileServices": { + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "properties": { + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]", + "protocolSettings": "[parameters('protocolSettings')]", + "shareDeleteRetentionPolicy": "[parameters('shareDeleteRetentionPolicy')]" + } + }, + "fileServices_diagnosticSettings": { + "copy": { + "name": "fileServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/fileServices/{1}', parameters('storageAccountName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "fileServices" + ] + }, + "fileServices_shares": { + "copy": { + "name": "fileServices_shares", + "count": "[length(coalesce(parameters('shares'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-shares-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "fileServicesName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('shares'), createArray())[copyIndex()].name]" + }, + "accessTier": { + "value": "[coalesce(tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'accessTier'), if(equals(reference('storageAccount', '2024-01-01', 'full').kind, 'FileStorage'), 'Premium', 'TransactionOptimized'))]" + }, + "enabledProtocols": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'enabledProtocols')]" + }, + "rootSquash": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'rootSquash')]" + }, + "shareQuota": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'shareQuota')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "15881640847294537074" + }, + "name": "Storage Account File Shares", + "description": "This module deploys a Storage Account File Share." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "fileServicesName": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Conditional. The name of the parent file service. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the file share to create." + } + }, + "accessTier": { + "type": "string", + "defaultValue": "TransactionOptimized", + "allowedValues": [ + "Premium", + "Hot", + "Cool", + "TransactionOptimized" + ], + "metadata": { + "description": "Conditional. Access tier for specific share. Required if the Storage Account kind is set to FileStorage (should be set to \"Premium\"). GpV2 account can choose between TransactionOptimized (default), Hot, and Cool." + } + }, + "shareQuota": { + "type": "int", + "defaultValue": 5120, + "metadata": { + "description": "Optional. The maximum size of the share, in gigabytes. Must be greater than 0, and less than or equal to 5120 (5TB). For Large File Shares, the maximum size is 102400 (100TB)." + } + }, + "enabledProtocols": { + "type": "string", + "defaultValue": "SMB", + "allowedValues": [ + "NFS", + "SMB" + ], + "metadata": { + "description": "Optional. The authentication protocol that is used for the file share. Can only be specified when creating a share." + } + }, + "rootSquash": { + "type": "string", + "defaultValue": "NoRootSquash", + "allowedValues": [ + "AllSquash", + "NoRootSquash", + "RootSquash" + ], + "metadata": { + "description": "Optional. Permissions for NFS file shares are enforced by the client OS rather than the Azure Files service. Toggling the root squash behavior reduces the rights of the root user for NFS shares." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage File Data SMB Share Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')]", + "Storage File Data SMB Share Elevated Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7264617-510b-434b-a828-9731dc254ea7')]", + "Storage File Data SMB Share Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'aba4ae5f-2193-4029-9191-0cb91df5e314')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::fileService": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('fileServicesName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.storage-fileshare.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "fileShare": { + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name'))]", + "properties": { + "accessTier": "[parameters('accessTier')]", + "shareQuota": "[parameters('shareQuota')]", + "rootSquash": "[if(equals(parameters('enabledProtocols'), 'NFS'), parameters('rootSquash'), null())]", + "enabledProtocols": "[parameters('enabledProtocols')]" + } + }, + "fileShare_roleAssignments": { + "copy": { + "name": "fileShare_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Share-Rbac-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "scope": { + "value": "[replace(resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name')), '/shares/', '/fileshares/')]" + }, + "name": { + "value": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]" + }, + "roleDefinitionId": { + "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" + }, + "principalId": { + "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "principalType": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]" + }, + "condition": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]" + }, + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), createObject('value', coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0')), createObject('value', null()))]", + "delegatedManagedIdentityResourceId": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "description": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "scope": { + "type": "string", + "metadata": { + "description": "Required. The scope to deploy the role assignment to." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the role assignment." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The role definition Id to assign." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User", + "" + ], + "defaultValue": "", + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"" + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "defaultValue": "2.0", + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[parameters('scope')]", + "name": "[parameters('name')]", + "properties": { + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "principalId": "[parameters('principalId')]", + "description": "[parameters('description')]", + "principalType": "[if(not(empty(parameters('principalType'))), parameters('principalType'), null())]", + "condition": "[if(not(empty(parameters('condition'))), parameters('condition'), null())]", + "conditionVersion": "[if(and(not(empty(parameters('conditionVersion'))), not(empty(parameters('condition')))), parameters('conditionVersion'), null())]", + "delegatedManagedIdentityResourceId": "[if(not(empty(parameters('delegatedManagedIdentityResourceId'))), parameters('delegatedManagedIdentityResourceId'), null())]" + } + } + ] + } + }, + "dependsOn": [ + "fileShare" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "fileServices", + "storageAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share service." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/fileServices', parameters('storageAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share service." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_queueServices": { + "condition": "[not(empty(parameters('queueServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-QueueServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('queueServices'), 'diagnosticSettings')]" + }, + "queues": { + "value": "[tryGet(parameters('queueServices'), 'queues')]" + }, + "corsRules": { + "value": "[tryGet(parameters('queueServices'), 'corsRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "1100093319443502715" + }, + "name": "Storage Account Queue Services", + "description": "This module deploys a Storage Account Queue Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "queues": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Queues to create." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "name": "default" + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "queueServices": { + "type": "Microsoft.Storage/storageAccounts/queueServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "properties": { + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]" + } + }, + "queueServices_diagnosticSettings": { + "copy": { + "name": "queueServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/queueServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "queueServices" + ] + }, + "queueServices_queues": { + "copy": { + "name": "queueServices_queues", + "count": "[length(coalesce(parameters('queues'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Queue-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "name": { + "value": "[coalesce(parameters('queues'), createArray())[copyIndex()].name]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('queues'), createArray())[copyIndex()], 'metadata')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('queues'), createArray())[copyIndex()], 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "17963799770990303971" + }, + "name": "Storage Account Queues", + "description": "This module deploys a Storage Account Queue." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage queue to deploy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair that represents queue metadata." + }, + "defaultValue": {} + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Queue Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "Storage Queue Data Message Processor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')]", + "Storage Queue Data Message Sender": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]", + "Storage Queue Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '19e7f393-937e-4f77-808e-94535e297925')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::queueServices": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/queueServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]" + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "queue": { + "type": "Microsoft.Storage/storageAccounts/queueServices/queues", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "properties": { + "metadata": "[parameters('metadata')]" + } + }, + "queue_roleAssignments": { + "copy": { + "name": "queue_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/queueServices/{1}/queues/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', parameters('storageAccountName'), 'default', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "queue" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed queue." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed queue." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', parameters('storageAccountName'), 'default', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed queue." + }, + "value": "[resourceGroup().name]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share service." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/queueServices', parameters('storageAccountName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share service." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_tableServices": { + "condition": "[not(empty(parameters('tableServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-TableServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('tableServices'), 'diagnosticSettings')]" + }, + "tables": { + "value": "[tryGet(parameters('tableServices'), 'tables')]" + }, + "corsRules": { + "value": "[tryGet(parameters('tableServices'), 'corsRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13069389074590786512" + }, + "name": "Storage Account Table Services", + "description": "This module deploys a Storage Account Table Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "tables": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. tables to create." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "name": "default" + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "tableServices": { + "type": "Microsoft.Storage/storageAccounts/tableServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "properties": { + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]" + } + }, + "tableServices_diagnosticSettings": { + "copy": { + "name": "tableServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/tableServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "tableServices" + ] + }, + "tableServices_tables": { + "copy": { + "name": "tableServices_tables", + "count": "[length(parameters('tables'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Table-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('tables')[copyIndex()].name]" + }, + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "roleAssignments": { + "value": "[tryGet(parameters('tables')[copyIndex()], 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10905926757212375091" + }, + "name": "Storage Account Table", + "description": "This module deploys a Storage Account Table." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the table." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Table Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "Storage Table Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76199698-9eea-4c19-bc75-cec21354c6b6')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::tableServices": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/tableServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]" + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "table": { + "type": "Microsoft.Storage/storageAccounts/tableServices/tables", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', parameters('name'))]" + }, + "table_roleAssignments": { + "copy": { + "name": "table_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/tableServices/{1}/tables/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/tableServices/tables', parameters('storageAccountName'), 'default', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "table" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share service." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/tableServices/tables', parameters('storageAccountName'), 'default', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share service." + }, + "value": "[resourceGroup().name]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed table service." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed table service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/tableServices', parameters('storageAccountName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed table service." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "secretsExport": { + "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", + "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" + }, + "secretsToSet": { + "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'accessKey1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey1Name'), 'value', listKeys('storageAccount', '2024-01-01').keys[0].value)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'connectionString1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'connectionString1Name'), 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2024-01-01').keys[0].value, environment().suffixes.storage))), createArray()), if(contains(parameters('secretsExportConfiguration'), 'accessKey2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey2Name'), 'value', listKeys('storageAccount', '2024-01-01').keys[1].value)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'connectionString2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'connectionString2Name'), 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2024-01-01').keys[1].value, environment().suffixes.storage))), createArray()))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9368972709899985618" + } + }, + "definitions": { + "secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "secretToSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret to set." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret to set." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the secret to set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Key Vault to set the ecrets in." + } + }, + "secretsToSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretToSetType" + }, + "metadata": { + "description": "Required. The secrets to set in the Key Vault." + } + } + }, + "resources": { + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "secrets": { + "copy": { + "name": "secrets", + "count": "[length(parameters('secretsToSet'))]" + }, + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "properties": { + "value": "[parameters('secretsToSet')[copyIndex()].value]" + } + } + }, + "outputs": { + "secretsSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretSetOutputType" + }, + "metadata": { + "description": "The references to the secrets exported to the provided Key Vault." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "input": { + "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", + "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]", + "secretUriWithVersion": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUriWithVersion]" + } + } + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed storage account." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed storage account." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed storage account." + }, + "value": "[resourceGroup().name]" + }, + "primaryBlobEndpoint": { + "type": "string", + "metadata": { + "description": "The primary blob endpoint reference if blob services are deployed." + }, + "value": "[if(and(not(empty(parameters('blobServices'))), contains(parameters('blobServices'), 'containers')), reference(format('Microsoft.Storage/storageAccounts/{0}', parameters('name')), '2019-04-01').primaryEndpoints.blob, '')]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('storageAccount', '2024-01-01', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('storageAccount', '2024-01-01', 'full').location]" + }, + "serviceEndpoints": { + "type": "object", + "metadata": { + "description": "All service endpoints of the deployed storage account, Note Standard_LRS and Standard_ZRS accounts only have a blob service endpoint." + }, + "value": "[reference('storageAccount').primaryEndpoints]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the Storage Account." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + }, + "exportedSecrets": { + "$ref": "#/definitions/secretsOutputType", + "metadata": { + "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." + }, + "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + }, + "primaryAccessKey": { + "type": "securestring", + "metadata": { + "description": "The primary access key of the storage account." + }, + "value": "[listKeys('storageAccount', '2024-01-01').keys[0].value]" + }, + "secondaryAccessKey": { + "type": "securestring", + "metadata": { + "description": "The secondary access key of the storage account." + }, + "value": "[listKeys('storageAccount', '2024-01-01').keys[1].value]" + }, + "primaryConnectionString": { + "type": "securestring", + "metadata": { + "description": "The primary connection string of the storage account." + }, + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2024-01-01').keys[0].value, environment().suffixes.storage)]" + }, + "secondaryConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary connection string of the storage account." + }, + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2024-01-01').keys[1].value, environment().suffixes.storage)]" + } + } + } + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "value": "[reference('storageAccount').outputs.resourceId.value]" + }, + "name": { + "type": "string", + "value": "[format('{0}.storageAccount', deployment().name)]" + }, + "primaryBlobEndpoint": { + "type": "string", + "value": "[reference('storageAccount').outputs.primaryBlobEndpoint.value]" + }, + "primaryQueueEndpoint": { + "type": "string", + "value": "[format('https://{0}.queue.{1}/', reference('storageAccount').outputs.name.value, environment().suffixes.storage)]" + }, + "privateEndpoints": { + "type": "array", + "value": "[reference('storageAccount').outputs.privateEndpoints.value]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "userAssignedIdentity" + ] + }, + "cosmos": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('cosmos-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "cosmosAccountName": { + "value": "[variables('cosmosAccountName')]" + }, + "cosmosDbName": { + "value": "[parameters('cosmosDbName')]" + }, + "cosmosDBContainerNames": { + "value": "[parameters('cosmosDBContainerNames')]" + }, + "cosmosDBDataContributorPrincipalIds": "[if(not(empty(parameters('principalId'))), createObject('value', createArray(reference('userAssignedIdentity').outputs.principalId.value, parameters('principalId'))), createObject('value', createArray(reference('userAssignedIdentity').outputs.principalId.value)))]", + "zoneRedundant": { + "value": false + }, + "enablePrivateEndpoint": { + "value": "[variables('isAILZIntegrated')]" + }, + "privateEndpointSubnetId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').privateEndpointSubnetId), createObject('value', ''))]", + "cosmosPrivateDnsZoneId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').privateDnsZoneIds.cosmos), createObject('value', ''))]", + "publicNetworkAccess": "[if(variables('isAILZIntegrated'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "8132754725894220244" + } + }, + "parameters": { + "location": { + "type": "string", + "metadata": { + "description": "Location for cosmos DB resources" + } + }, + "cosmosAccountName": { + "type": "string", + "metadata": { + "description": "Required: Cosmos DB account name" + } + }, + "cosmosDbName": { + "type": "string", + "metadata": { + "description": "Required: Cosmos DB database name" + } + }, + "cosmosDBContainerNames": { + "type": "array", + "metadata": { + "description": "Required: Cosmos DB container names used in the application" + } + }, + "cosmosDBDataContributorPrincipalIds": { + "type": "array", + "metadata": { + "description": "Required: List of principal IDs (managed identity or user) to be assigned Cosmos DB SQL Data Contributor role" + } + }, + "zoneRedundant": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional: Enable zone redundancy for Cosmos DB account. Defaults to false." + } + }, + "enablePrivateEndpoint": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional: Enable private endpoint for AILZ integrated mode. Defaults to false." + } + }, + "privateEndpointSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional: Subnet ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "cosmosPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional: Cosmos DB Private DNS Zone ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional: Public network access setting (used in AILZ integrated mode). Defaults to Enabled." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional: Tags for resources" + } + } + }, + "resources": { + "cosmosAccount": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}.cosmosAccount', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('cosmosAccountName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "capabilitiesToAdd": { + "value": [ + "EnableServerless" + ] + }, + "databaseAccountOfferType": { + "value": "Standard" + }, + "disableLocalAuthentication": { + "value": true + }, + "automaticFailover": { + "value": false + }, + "enableMultipleWriteLocations": { + "value": false + }, + "enableFreeTier": { + "value": false + }, + "defaultConsistencyLevel": { + "value": "Session" + }, + "backupPolicyContinuousTier": { + "value": "Continuous7Days" + }, + "networkRestrictions": { + "value": { + "publicNetworkAccess": "[parameters('publicNetworkAccess')]", + "networkAclBypass": "AzureServices", + "ipRules": [] + } + }, + "privateEndpoints": "[if(parameters('enablePrivateEndpoint'), createObject('value', createArray(createObject('name', format('{0}-pe', parameters('cosmosAccountName')), 'resourceGroupResourceId', resourceGroup().id, 'subnetResourceId', parameters('privateEndpointSubnetId'), 'service', 'Sql', 'privateLinkServiceConnectionName', format('{0}-cosmos-plsc', parameters('cosmosAccountName')), 'privateDnsZoneGroups', createArray(createObject('name', 'cosmos-dns-zone-group', 'privateDnsZoneGroupConfigs', if(not(empty(parameters('cosmosPrivateDnsZoneId'))), createArray(createObject('name', 'cosmos-config', 'privateDnsZoneResourceId', parameters('cosmosPrivateDnsZoneId'))), createArray())))))), createObject('value', createArray()))]", + "zoneRedundant": { + "value": "[parameters('zoneRedundant')]" + }, + "sqlDatabases": { + "value": [ + { + "copy": [ + { + "name": "containers", + "count": "[length(parameters('cosmosDBContainerNames'))]", + "input": { + "name": "[parameters('cosmosDBContainerNames')[copyIndex('containers')].name]", + "paths": [ + "[parameters('cosmosDBContainerNames')[copyIndex('containers')].partitionKey]" + ], + "kind": "Hash" + } + } + ], + "name": "[parameters('cosmosDbName')]" + } + ] + }, + "dataPlaneRoleDefinitions": { + "value": [ + { + "copy": [ + { + "name": "assignments", + "count": "[length(parameters('cosmosDBDataContributorPrincipalIds'))]", + "input": { + "principalId": "[parameters('cosmosDBDataContributorPrincipalIds')[copyIndex('assignments')]]" + } + } + ], + "roleName": "Cosmos DB SQL Data Contributor", + "dataActions": [ + "Microsoft.DocumentDB/databaseAccounts/readMetadata", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*", + "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*" + ] + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "17715929342484596741" + }, + "name": "Azure Cosmos DB account", + "description": "This module deploys an Azure Cosmos DB account. The API used for the account is determined by the child resources that are deployed." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group ID for the private endpoint group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "fully-qualified domain name (FQDN) that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses for the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the private endpoint output." + } + }, + "failoverLocationType": { + "type": "object", + "properties": { + "failoverPriority": { + "type": "int", + "metadata": { + "description": "Required. The failover priority of the region. A failover priority of 0 indicates a write region. The maximum value for a failover priority = (total number of regions - 1). Failover priority values must be unique for each of the regions in which the database account exists." + } + }, + "isZoneRedundant": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Flag to indicate whether or not this region is an AvailabilityZone region. Defaults to true." + } + }, + "locationName": { + "type": "string", + "metadata": { + "description": "Required. The name of the region." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the failover location." + } + }, + "dataPlaneRoleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique name of the role assignment." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier of the Azure Cosmos DB for NoSQL native role-based access control definition." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated Microsoft Entra ID principal to which access is being granted through this role-based access control assignment. The tenant ID for the principal is inferred using the tenant associated with the subscription." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB for NoSQL native role-based access control assignment." + } + }, + "dataPlaneRoleDefinitionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique identifier of the role-based access control definition." + } + }, + "roleName": { + "type": "string", + "metadata": { + "description": "Required. A user-friendly name for the role-based access control definition. This must be unique within the database account." + } + }, + "dataActions": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of data actions that are allowed." + } + }, + "assignableScopes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A set of fully-qualified scopes at or below which role-based access control assignments may be created using this definition. This setting allows application of this definition on the entire account or any underlying resource. This setting must have at least one element. Scopes higher than the account level are not enforceable as assignable scopes. Resources referenced in assignable scopes do not need to exist at creation. Defaults to the current account scope." + } + }, + "assignments": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of role-based access control assignments to be created for the definition." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB for NoSQL or Table native role-based access control definition." + } + }, + "sqlDatabaseType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the database ." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request units per second. Will be ignored if `autoscaleSettingsMaxThroughput` is used. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level. Defaults to 400." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the autoscale settings and represents maximum throughput the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If the value is not set, then autoscale will be disabled. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "containers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the container." + } + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "maxLength": 3, + "metadata": { + "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." + } + }, + "analyticalStorageTtl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "maxValue": 1000000, + "metadata": { + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level." + } + }, + "conflictResolutionPolicy": { + "type": "object", + "properties": { + "conflictResolutionPath": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The conflict resolution path in the case of LastWriterWins mode. Required if `mode` is set to 'LastWriterWins'." + } + }, + "conflictResolutionProcedure": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The procedure to resolve conflicts in the case of custom mode. Required if `mode` is set to 'Custom'." + } + }, + "mode": { + "type": "string", + "allowedValues": [ + "Custom", + "LastWriterWins" + ], + "metadata": { + "description": "Required. Indicates the conflict resolution mode." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." + } + }, + "defaultTtl": { + "type": "int", + "nullable": true, + "minValue": -1, + "maxValue": 2147483647, + "metadata": { + "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." + } + }, + "indexingPolicy": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Indexing policy of the container." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "Hash", + "MultiHash" + ], + "nullable": true, + "metadata": { + "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." + } + }, + "version": { + "type": "int", + "allowedValues": [ + 1, + 2 + ], + "nullable": true, + "metadata": { + "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used." + } + }, + "uniqueKeyPolicyKeys": { + "type": "array", + "items": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. List of paths must be unique for each document in the Azure Cosmos DB service." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Set of containers to deploy in the database." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB for NoSQL database." + } + }, + "networkRestrictionType": { + "type": "object", + "properties": { + "ipRules": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A single IPv4 address or a single IPv4 address range in Classless Inter-Domain Routing (CIDR) format. Provided IPs must be well-formatted and cannot be contained in one of the following ranges: `10.0.0.0/8`, `100.64.0.0/10`, `172.16.0.0/12`, `192.168.0.0/16`, since these are not enforceable by the IP address filter. Example of valid inputs: `23.40.210.245` or `23.40.210.0/8`." + } + }, + "networkAclBypass": { + "type": "string", + "allowedValues": [ + "AzureServices", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the network ACL bypass for Azure services. Default to \"None\"." + } + }, + "publicNetworkAccess": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "nullable": true, + "metadata": { + "description": "Optional. Whether requests from the public network are allowed. Default to \"Disabled\"." + } + }, + "virtualNetworkRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of a subnet." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. List of virtual network access control list (ACL) rules configured for the account." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the network restriction." + } + }, + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "privateEndpointMultiServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the private endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The subresource to deploy the private endpoint for. For example \"blob\", \"table\", \"queue\" or \"file\" for a Storage Account's Private Endpoints." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can NOT be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "sqlRoleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + } + }, + "metadata": { + "description": "The type for the SQL Role Assignments.", + "__bicep_imported_from!": { + "sourceTemplate": "sql-role-definition/main.bicep" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the account." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Defaults to the current resource group scope location. Location for all resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts@2024-11-15#properties/tags" + }, + "description": "Optional. Tags for the resource." + }, + "nullable": true + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "databaseAccountOfferType": { + "type": "string", + "defaultValue": "Standard", + "allowedValues": [ + "Standard" + ], + "metadata": { + "description": "Optional. The offer type for the account. Defaults to \"Standard\"." + } + }, + "failoverLocations": { + "type": "array", + "items": { + "$ref": "#/definitions/failoverLocationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The set of locations enabled for the account. Defaults to the location where the account is deployed." + } + }, + "zoneRedundant": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether the single-region account is zone redundant. Defaults to true. This property is ignored for multi-region accounts." + } + }, + "defaultConsistencyLevel": { + "type": "string", + "defaultValue": "Session", + "allowedValues": [ + "Eventual", + "ConsistentPrefix", + "Session", + "BoundedStaleness", + "Strong" + ], + "metadata": { + "description": "Optional. The default consistency level of the account. Defaults to \"Session\"." + } + }, + "disableLocalAuthentication": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Opt-out of local authentication and ensure that only Microsoft Entra can be used exclusively for authentication. Defaults to true." + } + }, + "enableAnalyticalStorage": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Flag to indicate whether to enable storage analytics. Defaults to false." + } + }, + "automaticFailover": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable automatic failover for regions. Defaults to true." + } + }, + "enableFreeTier": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Flag to indicate whether \"Free Tier\" is enabled. Defaults to false." + } + }, + "enableMultipleWriteLocations": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables the account to write in multiple locations. Periodic backup must be used if enabled. Defaults to false." + } + }, + "disableKeyBasedMetadataWriteAccess": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Disable write operations on metadata resources (databases, containers, throughput) via account keys. Defaults to true." + } + }, + "maxStalenessPrefix": { + "type": "int", + "defaultValue": 100000, + "minValue": 1, + "maxValue": 2147483647, + "metadata": { + "description": "Optional. The maximum stale requests. Required for \"BoundedStaleness\" consistency level. Valid ranges, Single Region: 10 to 1000000. Multi Region: 100000 to 1000000. Defaults to 100000." + } + }, + "maxIntervalInSeconds": { + "type": "int", + "defaultValue": 300, + "minValue": 5, + "maxValue": 86400, + "metadata": { + "description": "Optional. The maximum lag time in minutes. Required for \"BoundedStaleness\" consistency level. Valid ranges, Single Region: 5 to 84600. Multi Region: 300 to 86400. Defaults to 300." + } + }, + "serverVersion": { + "type": "string", + "defaultValue": "4.2", + "allowedValues": [ + "3.2", + "3.6", + "4.0", + "4.2", + "5.0", + "6.0", + "7.0" + ], + "metadata": { + "description": "Optional. Specifies the MongoDB server version to use if using Azure Cosmos DB for MongoDB RU. Defaults to \"4.2\"." + } + }, + "sqlDatabases": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlDatabaseType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration for databases when using Azure Cosmos DB for NoSQL." + } + }, + "mongodbDatabases": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Configuration for databases when using Azure Cosmos DB for MongoDB RU." + } + }, + "gremlinDatabases": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Configuration for databases when using Azure Cosmos DB for Apache Gremlin." + } + }, + "tables": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Configuration for databases when using Azure Cosmos DB for Table." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "totalThroughputLimit": { + "type": "int", + "defaultValue": -1, + "metadata": { + "description": "Optional. The total throughput limit imposed on this account in request units per second (RU/s). Default to unlimited throughput." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of control plane Azure role-based access control assignments." + } + }, + "dataPlaneRoleDefinitions": { + "type": "array", + "items": { + "$ref": "#/definitions/dataPlaneRoleDefinitionType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configurations for Azure Cosmos DB for NoSQL native role-based access control definitions. Allows the creations of custom role definitions." + } + }, + "dataPlaneRoleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/dataPlaneRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configurations for Azure Cosmos DB for NoSQL native role-based access control assignments." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings for the service." + } + }, + "capabilitiesToAdd": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "allowedValues": [ + "EnableCassandra", + "EnableTable", + "EnableGremlin", + "EnableMongo", + "DisableRateLimitingResponses", + "EnableServerless", + "EnableNoSQLVectorSearch", + "EnableNoSQLFullTextSearch", + "EnableMaterializedViews", + "DeleteAllItemsByPartitionKey" + ], + "metadata": { + "description": "Optional. A list of Azure Cosmos DB specific capabilities for the account." + } + }, + "backupPolicyType": { + "type": "string", + "defaultValue": "Continuous", + "allowedValues": [ + "Periodic", + "Continuous" + ], + "metadata": { + "description": "Optional. Configures the backup mode. Periodic backup must be used if multiple write locations are used. Defaults to \"Continuous\"." + } + }, + "backupPolicyContinuousTier": { + "type": "string", + "defaultValue": "Continuous30Days", + "allowedValues": [ + "Continuous30Days", + "Continuous7Days" + ], + "metadata": { + "description": "Optional. Configuration values to specify the retention period for continuous mode backup. Default to \"Continuous30Days\"." + } + }, + "backupIntervalInMinutes": { + "type": "int", + "defaultValue": 240, + "minValue": 60, + "maxValue": 1440, + "metadata": { + "description": "Optional. An integer representing the interval in minutes between two backups. This setting only applies to the periodic backup type. Defaults to 240." + } + }, + "backupRetentionIntervalInHours": { + "type": "int", + "defaultValue": 8, + "minValue": 2, + "maxValue": 720, + "metadata": { + "description": "Optional. An integer representing the time (in hours) that each backup is retained. This setting only applies to the periodic backup type. Defaults to 8." + } + }, + "backupStorageRedundancy": { + "type": "string", + "defaultValue": "Local", + "allowedValues": [ + "Geo", + "Local", + "Zone" + ], + "metadata": { + "description": "Optional. Setting that indicates the type of backup residency. This setting only applies to the periodic backup type. Defaults to \"Local\"." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointMultiServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is advised to use private endpoints whenever possible." + } + }, + "networkRestrictions": { + "$ref": "#/definitions/networkRestrictionType", + "defaultValue": { + "ipRules": [], + "virtualNetworkRules": [], + "publicNetworkAccess": "Disabled" + }, + "metadata": { + "description": "Optional. The network configuration of this module. Defaults to `{ ipRules: [], virtualNetworkRules: [], publicNetworkAccess: 'Disabled' }`." + } + }, + "minimumTlsVersion": { + "type": "string", + "defaultValue": "Tls12", + "allowedValues": [ + "Tls12" + ], + "metadata": { + "description": "Optional. Setting that indicates the minimum allowed TLS version. Azure Cosmos DB for MongoDB RU and Apache Cassandra only work with TLS 1.2 or later. Defaults to \"Tls12\" (TLS 1.2)." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInControlPlaneRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInControlPlaneRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Cosmos DB Account Reader Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fbdf93bf-df7d-467e-a4d2-9458aa1360c8')]", + "Cosmos DB Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa')]", + "CosmosBackupOperator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db7b14f2-5adf-42da-9f96-f2ee17bab5cb')]", + "CosmosRestoreOperator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5432c526-bc82-444a-b7ba-57c5b0b5b34f')]", + "DocumentDB Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5bd9cd88-fe45-4216-938b-f97437e15450')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-07-01", + "name": "[format('46d3xbcp.res.documentdb-databaseaccount.{0}.{1}', replace('0.16.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", + "kind": "[if(not(empty(parameters('mongodbDatabases'))), 'MongoDB', 'GlobalDocumentDB')]", + "properties": "[shallowMerge(createArray(createObject('databaseAccountOfferType', parameters('databaseAccountOfferType'), 'backupPolicy', shallowMerge(createArray(createObject('type', parameters('backupPolicyType')), if(equals(parameters('backupPolicyType'), 'Continuous'), createObject('continuousModeProperties', createObject('tier', parameters('backupPolicyContinuousTier'))), createObject()), if(equals(parameters('backupPolicyType'), 'Periodic'), createObject('periodicModeProperties', createObject('backupIntervalInMinutes', parameters('backupIntervalInMinutes'), 'backupRetentionIntervalInHours', parameters('backupRetentionIntervalInHours'), 'backupStorageRedundancy', parameters('backupStorageRedundancy'))), createObject()))), 'capabilities', map(coalesce(parameters('capabilitiesToAdd'), createArray()), lambda('capability', createObject('name', lambdaVariables('capability')))), 'minimalTlsVersion', parameters('minimumTlsVersion'), 'capacity', createObject('totalThroughputLimit', parameters('totalThroughputLimit')), 'publicNetworkAccess', coalesce(tryGet(parameters('networkRestrictions'), 'publicNetworkAccess'), 'Disabled')), if(or(or(or(not(empty(parameters('sqlDatabases'))), not(empty(parameters('mongodbDatabases')))), not(empty(parameters('gremlinDatabases')))), not(empty(parameters('tables')))), createObject('consistencyPolicy', shallowMerge(createArray(createObject('defaultConsistencyLevel', parameters('defaultConsistencyLevel')), if(equals(parameters('defaultConsistencyLevel'), 'BoundedStaleness'), createObject('maxStalenessPrefix', parameters('maxStalenessPrefix'), 'maxIntervalInSeconds', parameters('maxIntervalInSeconds')), createObject()))), 'enableMultipleWriteLocations', parameters('enableMultipleWriteLocations'), 'locations', if(not(empty(parameters('failoverLocations'))), map(parameters('failoverLocations'), lambda('failoverLocation', createObject('failoverPriority', lambdaVariables('failoverLocation').failoverPriority, 'locationName', lambdaVariables('failoverLocation').locationName, 'isZoneRedundant', coalesce(tryGet(lambdaVariables('failoverLocation'), 'isZoneRedundant'), true())))), createArray(createObject('failoverPriority', 0, 'locationName', parameters('location'), 'isZoneRedundant', parameters('zoneRedundant')))), 'ipRules', map(coalesce(tryGet(parameters('networkRestrictions'), 'ipRules'), createArray()), lambda('ipRule', createObject('ipAddressOrRange', lambdaVariables('ipRule')))), 'virtualNetworkRules', map(coalesce(tryGet(parameters('networkRestrictions'), 'virtualNetworkRules'), createArray()), lambda('rule', createObject('id', lambdaVariables('rule').subnetResourceId, 'ignoreMissingVNetServiceEndpoint', false()))), 'networkAclBypass', coalesce(tryGet(parameters('networkRestrictions'), 'networkAclBypass'), 'None'), 'isVirtualNetworkFilterEnabled', or(not(empty(tryGet(parameters('networkRestrictions'), 'ipRules'))), not(empty(tryGet(parameters('networkRestrictions'), 'virtualNetworkRules')))), 'enableFreeTier', parameters('enableFreeTier'), 'enableAutomaticFailover', parameters('automaticFailover'), 'enableAnalyticalStorage', parameters('enableAnalyticalStorage')), createObject()), if(or(not(empty(parameters('mongodbDatabases'))), not(empty(parameters('gremlinDatabases')))), createObject('disableLocalAuth', false(), 'disableKeyBasedMetadataWriteAccess', false()), createObject('disableLocalAuth', parameters('disableLocalAuthentication'), 'disableKeyBasedMetadataWriteAccess', parameters('disableKeyBasedMetadataWriteAccess'))), if(not(empty(parameters('mongodbDatabases'))), createObject('apiProperties', createObject('serverVersion', parameters('serverVersion'))), createObject())))]" + }, + "databaseAccount_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_diagnosticSettings": { + "copy": { + "name": "databaseAccount_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_roleAssignments": { + "copy": { + "name": "databaseAccount_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_sqlDatabases": { + "copy": { + "name": "databaseAccount_sqlDatabases", + "count": "[length(coalesce(parameters('sqlDatabases'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqldb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('sqlDatabases'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('sqlDatabases'), createArray())[copyIndex()].name]" + }, + "containers": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'containers')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'throughput')]" + }, + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "7141543733238879531" + }, + "name": "DocumentDB Database Account SQL Databases", + "description": "This module deploys a SQL Database in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the SQL database ." + } + }, + "containers": { + "type": "array", + "items": { + "type": "object" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of containers to deploy in the SQL database." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the SQL database resource." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]" + }, + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', if(equals(parameters('autoscaleSettingsMaxThroughput'), null()), parameters('throughput'), null()), 'autoscaleSettings', if(not(equals(parameters('autoscaleSettingsMaxThroughput'), null())), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null())))]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "container": { + "copy": { + "name": "container", + "count": "[length(coalesce(parameters('containers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqldb-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('containers'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "sqlDatabaseName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('containers'), createArray())[copyIndex()].name]" + }, + "analyticalStorageTtl": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'analyticalStorageTtl')]" + }, + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + }, + "conflictResolutionPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'conflictResolutionPolicy')]" + }, + "defaultTtl": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'defaultTtl')]" + }, + "indexingPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'indexingPolicy')]" + }, + "kind": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'kind')]" + }, + "version": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'version')]" + }, + "paths": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'paths')]" + }, + "throughput": "[if(and(or(not(equals(parameters('throughput'), null())), not(equals(parameters('autoscaleSettingsMaxThroughput'), null()))), equals(tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'throughput'), null())), createObject('value', -1), createObject('value', tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'throughput')))]", + "uniqueKeyPolicyKeys": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'uniqueKeyPolicyKeys')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "1789954443166349986" + }, + "name": "DocumentDB Database Account SQL Database Containers", + "description": "This module deploys a SQL Database Container in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "sqlDatabaseName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent SQL Database. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the container." + } + }, + "analyticalStorageTtl": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." + } + }, + "conflictResolutionPolicy": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." + } + }, + "defaultTtl": { + "type": "int", + "defaultValue": -1, + "minValue": -1, + "maxValue": 2147483647, + "metadata": { + "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." + } + }, + "throughput": { + "type": "int", + "defaultValue": 400, + "metadata": { + "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "maxValue": 1000000, + "metadata": { + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the SQL Database resource." + } + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "maxLength": 3, + "metadata": { + "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." + } + }, + "indexingPolicy": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Indexing policy of the container." + } + }, + "uniqueKeyPolicyKeys": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." + } + }, + "kind": { + "type": "string", + "defaultValue": "Hash", + "allowedValues": [ + "Hash", + "MultiHash" + ], + "metadata": { + "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." + } + }, + "version": { + "type": "int", + "defaultValue": 1, + "allowedValues": [ + 1, + 2 + ], + "metadata": { + "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." + } + } + }, + "variables": { + "copy": [ + { + "name": "partitionKeyPaths", + "count": "[length(parameters('paths'))]", + "input": "[if(startsWith(parameters('paths')[copyIndex('partitionKeyPaths')], '/'), parameters('paths')[copyIndex('partitionKeyPaths')], format('/{0}', parameters('paths')[copyIndex('partitionKeyPaths')]))]" + } + ], + "containerResourceParams": "[union(createObject('conflictResolutionPolicy', parameters('conflictResolutionPolicy'), 'defaultTtl', parameters('defaultTtl'), 'id', parameters('name'), 'indexingPolicy', if(not(empty(parameters('indexingPolicy'))), parameters('indexingPolicy'), null()), 'partitionKey', createObject('paths', variables('partitionKeyPaths'), 'kind', parameters('kind'), 'version', if(equals(parameters('kind'), 'MultiHash'), 2, parameters('version'))), 'uniqueKeyPolicy', if(not(empty(parameters('uniqueKeyPolicyKeys'))), createObject('uniqueKeys', parameters('uniqueKeyPolicyKeys')), null())), if(not(equals(parameters('analyticalStorageTtl'), 0)), createObject('analyticalStorageTtl', parameters('analyticalStorageTtl')), createObject()))]" + }, + "resources": { + "databaseAccount::sqlDatabase": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('sqlDatabaseName'))]" + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "container": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('sqlDatabaseName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": "[variables('containerResourceParams')]", + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', if(and(equals(parameters('autoscaleSettingsMaxThroughput'), null()), not(equals(parameters('throughput'), -1))), parameters('throughput'), null()), 'autoscaleSettings', if(not(equals(parameters('autoscaleSettingsMaxThroughput'), null())), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null())))]" + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the container." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the container." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('databaseAccountName'), parameters('sqlDatabaseName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the container was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "sqlDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL database." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_sqlRoleDefinitions": { + "copy": { + "name": "databaseAccount_sqlRoleDefinitions", + "count": "[length(coalesce(parameters('dataPlaneRoleDefinitions'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqlrd-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()], 'name')]" + }, + "dataActions": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()], 'dataActions')]" + }, + "roleName": { + "value": "[coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()].roleName]" + }, + "assignableScopes": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()], 'assignableScopes')]" + }, + "sqlRoleAssignments": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()], 'assignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9570871897890815068" + }, + "name": "DocumentDB Database Account SQL Role Definitions.", + "description": "This module deploys a SQL Role Definision in a CosmosDB Account." + }, + "definitions": { + "sqlRoleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the SQL Role Assignments." + } + } + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique identifier of the Role Definition." + } + }, + "roleName": { + "type": "string", + "metadata": { + "description": "Required. A user-friendly name for the Role Definition. Must be unique for the database account." + } + }, + "dataActions": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. An array of data actions that are allowed." + } + }, + "assignableScopes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A set of fully qualified Scopes at or below which Role Assignments may be created using this Role Definition. This will allow application of this Role Definition on the entire database account or any underlying Database / Collection. Must have at least one element. Scopes higher than Database account are not enforceable as assignable Scopes. Note that resources referenced in assignable Scopes need not exist. Defaults to the current account." + } + }, + "sqlRoleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of SQL Role Assignments to be created for the SQL Role Definition." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "enableReferencedModulesTelemetry": false + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroledefinition.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleDefinition": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), 'sql-role')))]", + "properties": { + "assignableScopes": "[coalesce(parameters('assignableScopes'), createArray(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]", + "permissions": [ + { + "dataActions": "[parameters('dataActions')]" + } + ], + "roleName": "[parameters('roleName')]", + "type": "CustomRole" + } + }, + "databaseAccount_sqlRoleAssignments": { + "copy": { + "name": "databaseAccount_sqlRoleAssignments", + "count": "[length(coalesce(parameters('sqlRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqlra-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "roleDefinitionId": { + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), 'sql-role')))]" + }, + "principalId": { + "value": "[coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()], 'name')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10102303164433641479" + }, + "name": "DocumentDB Database Account SQL Role Assignments.", + "description": "This module deploys a SQL Role Assignment in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier of the associated SQL Role Definition." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroleassignment.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Assignment." + }, + "value": "[coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL Role Assignment." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "sqlRoleDefinition" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Definition." + }, + "value": "[coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), 'sql-role'))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL Role Definition." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), 'sql-role')))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + }, + "roleName": { + "type": "string", + "metadata": { + "description": "The role name of the SQL Role Definition." + }, + "value": "[reference('sqlRoleDefinition').roleName]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_sqlRoleAssignments": { + "copy": { + "name": "databaseAccount_sqlRoleAssignments", + "count": "[length(coalesce(parameters('dataPlaneRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqlra-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "roleDefinitionId": { + "value": "[coalesce(parameters('dataPlaneRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" + }, + "principalId": { + "value": "[coalesce(parameters('dataPlaneRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleAssignments'), createArray())[copyIndex()], 'name')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10102303164433641479" + }, + "name": "DocumentDB Database Account SQL Role Assignments.", + "description": "This module deploys a SQL Role Assignment in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier of the associated SQL Role Definition." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroleassignment.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Assignment." + }, + "value": "[coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL Role Assignment." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_mongodbDatabases": { + "copy": { + "name": "databaseAccount_mongodbDatabases", + "count": "[length(coalesce(parameters('mongodbDatabases'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-mongodb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "collections": { + "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'collections')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9160691107424630312" + }, + "name": "DocumentDB Database Account MongoDB Databases", + "description": "This module deploys a MongoDB Database within a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Cosmos DB database account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the mongodb database." + } + }, + "throughput": { + "type": "int", + "defaultValue": 400, + "metadata": { + "description": "Optional. Request Units per second. Setting throughput at the database level is only recommended for development/test or when workload across all collections in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." + } + }, + "collections": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Collections in the mongodb database." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "mongodbDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]" + }, + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', parameters('throughput')))]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "mongodbDatabase_collections": { + "copy": { + "name": "mongodbDatabase_collections", + "count": "[length(coalesce(parameters('collections'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-collection-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('collections'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "mongodbDatabaseName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].name]" + }, + "indexes": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].indexes]" + }, + "shardKey": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].shardKey]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('collections'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "14050805189442830205" + }, + "name": "DocumentDB Database Account MongoDB Database Collections", + "description": "This module deploys a MongoDB Database Collection." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Cosmos DB database account. Required if the template is used in a standalone deployment." + } + }, + "mongodbDatabaseName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent mongodb database. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the collection." + } + }, + "throughput": { + "type": "int", + "defaultValue": 400, + "metadata": { + "description": "Optional. Request Units per second. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." + } + }, + "indexes": { + "type": "array", + "metadata": { + "description": "Required. Indexes for the collection." + } + }, + "shardKey": { + "type": "object", + "metadata": { + "description": "Required. ShardKey for the collection." + } + } + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('mongodbDatabaseName'), parameters('name'))]", + "properties": { + "options": "[if(contains(reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), '2024-11-15').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]", + "indexes": "[parameters('indexes')]", + "shardKey": "[parameters('shardKey')]" + } + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the mongodb database collection." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the mongodb database collection." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections', parameters('databaseAccountName'), parameters('mongodbDatabaseName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the mongodb database collection was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "mongodbDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the mongodb database." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the mongodb database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/mongodbDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the mongodb database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_gremlinDatabases": { + "copy": { + "name": "databaseAccount_gremlinDatabases", + "count": "[length(coalesce(parameters('gremlinDatabases'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-gremlin-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "graphs": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'graphs')]" + }, + "maxThroughput": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'maxThroughput')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "16834580070429190924" + }, + "name": "DocumentDB Database Account Gremlin Databases", + "description": "This module deploys a Gremlin Database within a CosmosDB Account." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Gremlin database." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the Gremlin database resource." + } + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Gremlin database. Required if the template is used in a standalone deployment." + } + }, + "graphs": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. Array of graphs to deploy in the Gremlin database." + } + }, + "maxThroughput": { + "type": "int", + "defaultValue": 4000, + "metadata": { + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "gremlinDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('maxThroughput')), null()), 'throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]" + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "gremlinDatabase_gremlinGraphs": { + "copy": { + "name": "gremlinDatabase_gremlinGraphs", + "count": "[length(parameters('graphs'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-gremlindb-{1}', uniqueString(deployment().name, parameters('name')), parameters('graphs')[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('graphs')[copyIndex()].name]" + }, + "gremlinDatabaseName": { + "value": "[parameters('name')]" + }, + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "indexingPolicy": { + "value": "[tryGet(parameters('graphs')[copyIndex()], 'indexingPolicy')]" + }, + "partitionKeyPaths": "[if(not(empty(parameters('graphs')[copyIndex()].partitionKeyPaths)), createObject('value', parameters('graphs')[copyIndex()].partitionKeyPaths), createObject('value', createArray()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "15062578211366932944" + }, + "name": "DocumentDB Database Accounts Gremlin Databases Graphs", + "description": "This module deploys a DocumentDB Database Accounts Gremlin Database Graph." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the graph." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the Gremlin graph resource." + } + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "gremlinDatabaseName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Gremlin Database. Required if the template is used in a standalone deployment." + } + }, + "indexingPolicy": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Indexing policy of the graph." + } + }, + "partitionKeyPaths": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. List of paths using which data within the container can be partitioned." + } + } + }, + "resources": { + "databaseAccount::gremlinDatabase": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('gremlinDatabaseName'))]" + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "gremlinGraph": { + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('gremlinDatabaseName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]", + "indexingPolicy": "[if(not(empty(parameters('indexingPolicy'))), parameters('indexingPolicy'), null())]", + "partitionKey": { + "paths": "[if(not(empty(parameters('partitionKeyPaths'))), parameters('partitionKeyPaths'), null())]" + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the graph." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the graph." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs', parameters('databaseAccountName'), parameters('gremlinDatabaseName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the graph was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "gremlinDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the Gremlin database." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Gremlin database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/gremlinDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Gremlin database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_tables": { + "copy": { + "name": "databaseAccount_tables", + "count": "[length(coalesce(parameters('tables'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-table-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('tables'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('tables'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "maxThroughput": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'maxThroughput')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "3429971823201332257" + }, + "name": "Azure Cosmos DB account tables", + "description": "This module deploys a table within an Azure Cosmos DB Account." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the table." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags for the table." + } + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Azure Cosmos DB account. Required if the template is used in a standalone deployment." + } + }, + "maxThroughput": { + "type": "int", + "defaultValue": 4000, + "metadata": { + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "table": { + "type": "Microsoft.DocumentDB/databaseAccounts/tables", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('maxThroughput')), null()), 'throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]" + } + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the table." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the table." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/tables', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the table was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_privateEndpoints": { + "copy": { + "name": "databaseAccount_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-dbAccount-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the database account." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the database account." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the database account was created in." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('databaseAccount', '2024-11-15', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('databaseAccount', '2024-11-15', 'full').location]" + }, + "endpoint": { + "type": "string", + "metadata": { + "description": "The endpoint of the database account." + }, + "value": "[reference('databaseAccount').documentEndpoint]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the database account." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + }, + "primaryReadWriteKey": { + "type": "securestring", + "metadata": { + "description": "The primary read-write key." + }, + "value": "[listKeys('databaseAccount', '2024-11-15').primaryMasterKey]" + }, + "primaryReadOnlyKey": { + "type": "securestring", + "metadata": { + "description": "The primary read-only key." + }, + "value": "[listKeys('databaseAccount', '2024-11-15').primaryReadonlyMasterKey]" + }, + "primaryReadWriteConnectionString": { + "type": "securestring", + "metadata": { + "description": "The primary read-write connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2024-11-15').connectionStrings[0].connectionString]" + }, + "primaryReadOnlyConnectionString": { + "type": "securestring", + "metadata": { + "description": "The primary read-only connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2024-11-15').connectionStrings[2].connectionString]" + }, + "secondaryReadWriteKey": { + "type": "securestring", + "metadata": { + "description": "The secondary read-write key." + }, + "value": "[listKeys('databaseAccount', '2024-11-15').secondaryMasterKey]" + }, + "secondaryReadOnlyKey": { + "type": "securestring", + "metadata": { + "description": "The secondary read-only key." + }, + "value": "[listKeys('databaseAccount', '2024-11-15').secondaryReadonlyMasterKey]" + }, + "secondaryReadWriteConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary read-write connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2024-11-15').connectionStrings[1].connectionString]" + }, + "secondaryReadOnlyConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary read-only connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2024-11-15').connectionStrings[3].connectionString]" + } + } + } + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "value": "[reference('cosmosAccount').outputs.resourceId.value]" + }, + "cosmosAccountName": { + "type": "string", + "value": "[format('{0}.cosmosAccount', deployment().name)]" + }, + "cosmosEndpoint": { + "type": "string", + "value": "[reference('cosmosAccount').outputs.endpoint.value]" + }, + "cosmosDBName": { + "type": "string", + "value": "[parameters('cosmosDbName')]" + }, + "privateEndpoints": { + "type": "array", + "value": "[reference('cosmosAccount').outputs.privateEndpoints.value]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "appConfigStore": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('appConfig-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appConfigStoreName": { + "value": "[variables('appConfigStoreName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "roleAssignedManagedIdentityPrincipalIds": { + "value": [ + "[reference('userAssignedIdentity').outputs.principalId.value]" + ] + }, + "enablePrivateEndpoint": { + "value": "[variables('isAILZIntegrated')]" + }, + "privateEndpointSubnetId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').privateEndpointSubnetId), createObject('value', ''))]", + "appConfigPrivateDnsZoneId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').privateDnsZoneIds.appConfig), createObject('value', ''))]", + "publicNetworkAccess": "[if(variables('isAILZIntegrated'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "17272555307369828428" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources" + } + }, + "appConfigStoreName": { + "type": "string", + "metadata": { + "description": "App Configuration Store name" + } + }, + "roleAssignedManagedIdentityPrincipalIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Managed Identity that will be given access to the App Configuration Store" + } + }, + "enablePrivateEndpoint": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable private endpoint for AILZ mode" + } + }, + "privateEndpointSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Subnet ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "appConfigPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "App Configuration Private DNS Zone ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Public network access setting" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for resources" + } + } + }, + "variables": { + "copy": [ + { + "name": "roleAssignments", + "count": "[length(parameters('roleAssignedManagedIdentityPrincipalIds'))]", + "input": { + "principalId": "[parameters('roleAssignedManagedIdentityPrincipalIds')[copyIndex('roleAssignments')]]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "App Configuration Data Reader" + } + } + ], + "deployerRoleAssignments": [ + { + "principalId": "[deployer().objectId]", + "principalType": "[if(empty(coalesce(tryGet(deployer(), 'userPrincipalName'), '')), 'ServicePrincipal', 'User')]", + "roleDefinitionIdOrName": "App Configuration Data Owner" + } + ] + }, + "resources": { + "appConfigStore": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-appConfigStore', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('appConfigStoreName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "sku": { + "value": "Standard" + }, + "createMode": { + "value": "Default" + }, + "disableLocalAuth": { + "value": false + }, + "dataPlaneProxy": { + "value": { + "authenticationMode": "Pass-through", + "privateLinkDelegation": "Disabled" + } + }, + "enablePurgeProtection": { + "value": false + }, + "softDeleteRetentionInDays": { + "value": 1 + }, + "roleAssignments": { + "value": "[concat(variables('roleAssignments'), variables('deployerRoleAssignments'))]" + }, + "publicNetworkAccess": { + "value": "[parameters('publicNetworkAccess')]" + }, + "privateEndpoints": "[if(parameters('enablePrivateEndpoint'), createObject('value', createArray(createObject('name', format('{0}-pe', parameters('appConfigStoreName')), 'resourceGroupResourceId', resourceGroup().id, 'subnetResourceId', parameters('privateEndpointSubnetId'), 'privateLinkServiceConnectionName', format('{0}-app-config-plsc', parameters('appConfigStoreName')), 'privateDnsZoneGroups', createArray(createObject('name', 'app-config-dns-zone-group', 'privateDnsZoneGroupConfigs', if(not(empty(parameters('appConfigPrivateDnsZoneId'))), createArray(createObject('name', 'app-config', 'privateDnsZoneResourceId', parameters('appConfigPrivateDnsZoneId'))), createArray())))))), createObject('value', createArray()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "396653159019145335" + }, + "name": "App Configuration Stores", + "description": "This module deploys an App Configuration Store." + }, + "definitions": { + "dataPlaneProxyType": { + "type": "object", + "properties": { + "authenticationMode": { + "type": "string", + "allowedValues": [ + "Local", + "Pass-through" + ], + "nullable": true, + "metadata": { + "description": "Optional. The data plane proxy authentication mode. This property manages the authentication mode of request to the data plane resources. 'Pass-through' is recommended." + } + }, + "privateLinkDelegation": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Required. The data plane proxy private link delegation. This property manages if a request from delegated Azure Resource Manager (ARM) private link is allowed when the data plane resource requires private link." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the data plane proxy." + } + }, + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "replicaLocationType": { + "type": "object", + "properties": { + "replicaLocation": { + "type": "string", + "metadata": { + "description": "Required. Location of the replica." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the replica." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a replica location" + } + }, + "_1.lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "customerManagedKeyWithAutoRotateType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + } + }, + "keyName": { + "type": "string", + "metadata": { + "description": "Required. The name of the customer managed key to use for encryption." + } + }, + "keyVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, using version as per 'autoRotationEnabled' setting." + } + }, + "autoRotationEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable auto-rotating to the latest key version. Default is `true`. If set to `false`, the latest key version at the time of the deployment is used." + } + }, + "userAssignedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type supports auto-rotation of the customer-managed key.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateEndpointSingleServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private Endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the Private Endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + } + }, + "lock": { + "$ref": "#/definitions/_1.lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Azure App Configuration." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "sku": { + "type": "string", + "defaultValue": "Standard", + "allowedValues": [ + "Free", + "Developer", + "Standard", + "Premium" + ], + "metadata": { + "description": "Optional. Pricing tier of App Configuration." + } + }, + "createMode": { + "type": "string", + "defaultValue": "Default", + "allowedValues": [ + "Default", + "Recover" + ], + "metadata": { + "description": "Optional. Indicates whether the configuration store need to be recovered." + } + }, + "disableLocalAuth": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Disables all authentication methods other than AAD authentication." + } + }, + "enablePurgeProtection": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Property specifying whether protection against purge is enabled for this configuration store. Defaults to true unless sku is set to Free, since purge protection is not available in Free tier." + } + }, + "publicNetworkAccess": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set." + } + }, + "softDeleteRetentionInDays": { + "type": "int", + "defaultValue": 1, + "minValue": 1, + "maxValue": 7, + "metadata": { + "description": "Optional. The amount of time in days that the configuration store will be retained when it is soft deleted." + } + }, + "customerManagedKey": { + "$ref": "#/definitions/customerManagedKeyWithAutoRotateType", + "nullable": true, + "metadata": { + "description": "Optional. The customer managed key definition." + } + }, + "keyValues": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. All Key / Values to create. Requires local authentication to be enabled." + } + }, + "replicaLocations": { + "type": "array", + "items": { + "$ref": "#/definitions/replicaLocationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. All Replicas to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.AppConfiguration/configurationStores@2024-05-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "dataPlaneProxy": { + "$ref": "#/definitions/dataPlaneProxyType", + "nullable": true, + "metadata": { + "description": "Optional. Property specifying the configuration of data plane proxy for Azure Resource Manager (ARM)." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "App Compliance Automation Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f37683f-2463-46b6-9ce7-9b788b988ba2')]", + "App Compliance Automation Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ffc6bbe0-e443-4c3b-bf54-26581bb2f78e')]", + "App Configuration Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b')]", + "App Configuration Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '516239f1-63e1-4d78-a4de-a74fb236a071')]", + "App Configuration Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '175b81b9-6e0d-490a-85e4-0d422273c10c')]", + "App Configuration Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fe86443c-f201-4fc4-9d2a-ac61149fbda0')]", + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "cMKKeyVault::cMKKey": { + "condition": "[and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), not(empty(tryGet(parameters('customerManagedKey'), 'keyName')))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('46d3xbcp.res.appconfiguration-configurationstore.{0}.{1}', replace('0.9.2', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "cMKKeyVault": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId')))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-12-01-preview", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + }, + "cMKUserAssignedIdentity": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + }, + "configurationStore": { + "type": "Microsoft.AppConfiguration/configurationStores", + "apiVersion": "2025-02-01-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('sku')]" + }, + "identity": "[variables('identity')]", + "properties": { + "createMode": "[parameters('createMode')]", + "disableLocalAuth": "[parameters('disableLocalAuth')]", + "enablePurgeProtection": "[if(or(equals(parameters('sku'), 'Free'), equals(parameters('sku'), 'Developer')), false(), parameters('enablePurgeProtection'))]", + "encryption": "[if(not(empty(parameters('customerManagedKey'))), createObject('keyVaultProperties', createObject('keyIdentifier', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), format('{0}/{1}', reference('cMKKeyVault::cMKKey').keyUri, parameters('customerManagedKey').keyVersion), if(coalesce(tryGet(parameters('customerManagedKey'), 'autoRotationEnabled'), true()), reference('cMKKeyVault::cMKKey').keyUri, reference('cMKKeyVault::cMKKey').keyUriWithVersion)), 'identityClientId', if(not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'))), reference('cMKUserAssignedIdentity').clientId, null()))), null())]", + "publicNetworkAccess": "[if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(not(empty(parameters('privateEndpoints'))), 'Disabled', 'Enabled'))]", + "softDeleteRetentionInDays": "[if(or(equals(parameters('sku'), 'Free'), equals(parameters('sku'), 'Developer')), 0, parameters('softDeleteRetentionInDays'))]", + "dataPlaneProxy": "[if(not(empty(parameters('dataPlaneProxy'))), createObject('authenticationMode', coalesce(tryGet(parameters('dataPlaneProxy'), 'authenticationMode'), 'Pass-through'), 'privateLinkDelegation', parameters('dataPlaneProxy').privateLinkDelegation), null())]" + }, + "dependsOn": [ + "cMKKeyVault::cMKKey", + "cMKUserAssignedIdentity" + ] + }, + "configurationStore_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.AppConfiguration/configurationStores/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "configurationStore" + ] + }, + "configurationStore_diagnosticSettings": { + "copy": { + "name": "configurationStore_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.AppConfiguration/configurationStores/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "configurationStore" + ] + }, + "configurationStore_roleAssignments": { + "copy": { + "name": "configurationStore_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.AppConfiguration/configurationStores/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.AppConfiguration/configurationStores', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "configurationStore" + ] + }, + "configurationStore_keyValues": { + "copy": { + "name": "configurationStore_keyValues", + "count": "[length(coalesce(parameters('keyValues'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-AppConfig-KeyValues-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appConfigurationName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('keyValues'), createArray())[copyIndex()].name]" + }, + "value": { + "value": "[coalesce(parameters('keyValues'), createArray())[copyIndex()].value]" + }, + "contentType": { + "value": "[tryGet(coalesce(parameters('keyValues'), createArray())[copyIndex()], 'contentType')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('keyValues'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "4166303424618131775" + }, + "name": "App Configuration Stores Key Values", + "description": "This module deploys an App Configuration Store Key Value." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the key." + } + }, + "value": { + "type": "string", + "metadata": { + "description": "Required. The value of the key-value." + } + }, + "appConfigurationName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent app configuration store. Required if the template is used in a standalone deployment." + } + }, + "contentType": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The content type of the key-values value. Providing a proper content-type can enable transformations of values when they are retrieved by applications." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + } + }, + "resources": { + "appConfiguration": { + "existing": true, + "type": "Microsoft.AppConfiguration/configurationStores", + "apiVersion": "2025-02-01-preview", + "name": "[parameters('appConfigurationName')]" + }, + "keyValues": { + "type": "Microsoft.AppConfiguration/configurationStores/keyValues", + "apiVersion": "2025-02-01-preview", + "name": "[format('{0}/{1}', parameters('appConfigurationName'), parameters('name'))]", + "properties": { + "contentType": "[parameters('contentType')]", + "tags": "[parameters('tags')]", + "value": "[parameters('value')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the key values." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the key values." + }, + "value": "[resourceId('Microsoft.AppConfiguration/configurationStores/keyValues', parameters('appConfigurationName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the batch account was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "configurationStore" + ] + }, + "configurationStore_replicas": { + "copy": { + "name": "configurationStore_replicas", + "count": "[length(coalesce(parameters('replicaLocations'), createArray()))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-AppConfig-Replicas-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appConfigurationName": { + "value": "[parameters('name')]" + }, + "replicaLocation": { + "value": "[coalesce(parameters('replicaLocations'), createArray())[copyIndex()].replicaLocation]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('replicaLocations'), createArray())[copyIndex()], 'name')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "12609356088985615301" + }, + "name": "App Configuration Replicas", + "description": "This module deploys an App Configuration Replica." + }, + "parameters": { + "name": { + "type": "string", + "defaultValue": "[format('{0}replica', parameters('replicaLocation'))]", + "metadata": { + "description": "Optional. Name of the replica." + } + }, + "appConfigurationName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent app configuration store. Required if the template is used in a standalone deployment." + } + }, + "replicaLocation": { + "type": "string", + "metadata": { + "description": "Required. Location of the replica." + } + } + }, + "resources": [ + { + "type": "Microsoft.AppConfiguration/configurationStores/replicas", + "apiVersion": "2025-02-01-preview", + "name": "[format('{0}/{1}', parameters('appConfigurationName'), parameters('name'))]", + "location": "[parameters('replicaLocation')]" + } + ], + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the app configuration was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the replica that was deployed." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the replica that was deployed." + }, + "value": "[resourceId('Microsoft.AppConfiguration/configurationStores/replicas', parameters('appConfigurationName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "configurationStore" + ] + }, + "configurationStore_privateEndpoints": { + "copy": { + "name": "configurationStore_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-configStore-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.AppConfiguration/configurationStores', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'configurationStores'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.AppConfiguration/configurationStores', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'configurationStores'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.AppConfiguration/configurationStores', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'configurationStores')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.AppConfiguration/configurationStores', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'configurationStores'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.AppConfiguration/configurationStores', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'configurationStores')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "configurationStore" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the app configuration." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the app configuration." + }, + "value": "[resourceId('Microsoft.AppConfiguration/configurationStores', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the app configuration store was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('configurationStore', '2025-02-01-preview', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('configurationStore', '2025-02-01-preview', 'full').location]" + }, + "endpoint": { + "type": "string", + "metadata": { + "description": "The endpoint of the app configuration." + }, + "value": "[reference('configurationStore').endpoint]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the app configuration." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('configurationStore_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('configurationStore_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('configurationStore_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('configurationStore_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('configurationStore_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + } + } + } + } + } + }, + "outputs": { + "endpoint": { + "type": "string", + "value": "[reference('appConfigStore').outputs.endpoint.value]" + }, + "resourceId": { + "type": "string", + "value": "[reference('appConfigStore').outputs.resourceId.value]" + }, + "name": { + "type": "string", + "value": "[reference('appConfigStore').outputs.name.value]" + }, + "privateEndpointIds": { + "type": "array", + "value": "[reference('appConfigStore').outputs.privateEndpoints.value]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "appConfigStoreKeys": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('appConfigKeys-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "appConfigStoreName": { + "value": "[variables('appConfigStoreName')]" + }, + "configurationKeyValues": { + "value": [ + { + "contentType": "text/plain", + "name": "contentflow.common.COSMOS_DB_ENDPOINT", + "value": "[reference('cosmos').outputs.cosmosEndpoint.value]" + }, + { + "contentType": "text/plain", + "name": "contentflow.common.COSMOS_DB_NAME", + "value": "[parameters('cosmosDbName')]" + }, + { + "contentType": "text/plain", + "name": "contentflow.common.BLOB_STORAGE_ACCOUNT_NAME", + "value": "[variables('storageAccountName')]" + }, + { + "contentType": "text/plain", + "name": "contentflow.common.BLOB_STORAGE_CONTAINER_NAME", + "value": "[parameters('docsContainerName')]" + }, + { + "contentType": "text/plain", + "name": "contentflow.common.STORAGE_ACCOUNT_WORKER_QUEUE_URL", + "value": "[reference('storage').outputs.primaryQueueEndpoint.value]" + }, + { + "contentType": "text/plain", + "name": "contentflow.common.STORAGE_WORKER_QUEUE_NAME", + "value": "[parameters('workerQueueName')]" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.API_ENABLED", + "value": "True" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.DEBUG", + "value": "False" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.LOG_LEVEL", + "value": "DEBUG" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.API_SERVER_HOST", + "value": "0.0.0.0" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.API_SERVER_PORT", + "value": "[format('{0}', parameters('apiContainerAppTargetPort'))]" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.API_SERVER_WORKERS", + "value": "1" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.CORS_ALLOW_CREDENTIALS", + "value": "true" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.CORS_ALLOW_ORIGINS", + "value": "*" + }, + { + "contentType": "text/plain", + "name": "contentflow.api.WORKER_ENGINE_API_ENDPOINT", + "value": "[format('https://{0}', reference('workerContainerApp').outputs.fqdn.value)]" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.NUM_PROCESSING_WORKERS", + "value": "4" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.NUM_SOURCE_WORKERS", + "value": "2" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.LOG_LEVEL", + "value": "INFO" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.DEBUG", + "value": "false" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.API_ENABLED", + "value": "[format('{0}', parameters('workerAPIEnabled'))]" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.API_HOST", + "value": "0.0.0.0" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.API_PORT", + "value": "[format('{0}', parameters('workerContainerAppTargetPort'))]" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.QUEUE_POLL_INTERVAL_SECONDS", + "value": "5" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.QUEUE_VISIBILITY_TIMEOUT_SECONDS", + "value": "300" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.QUEUE_MAX_MESSAGES", + "value": "32" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.MAX_TASK_RETRIES", + "value": "3" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.TASK_TIMEOUT_SECONDS", + "value": "600" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.DEFAULT_POLLING_INTERVAL_SECONDS", + "value": "60" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.SCHEDULER_SLEEP_INTERVAL_SECONDS", + "value": "5" + }, + { + "contentType": "text/plain", + "name": "contentflow.worker.LOCK_TTL_SECONDS", + "value": "300" + }, + { + "contentType": "text/plain", + "name": "sentinel", + "value": "1" + } + ] + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "8518542793451662129" + } + }, + "parameters": { + "appConfigStoreName": { + "type": "string" + }, + "configurationKeyValues": { + "type": "array" + } + }, + "resources": [ + { + "copy": { + "name": "keyValues", + "count": "[length(parameters('configurationKeyValues'))]" + }, + "type": "Microsoft.AppConfiguration/configurationStores/keyValues", + "apiVersion": "2024-06-01", + "name": "[format('{0}/{1}', parameters('appConfigStoreName'), parameters('configurationKeyValues')[copyIndex()].name)]", + "properties": { + "value": "[parameters('configurationKeyValues')[copyIndex()].value]", + "contentType": "[parameters('configurationKeyValues')[copyIndex()].contentType]" + } + } + ] + } + }, + "dependsOn": [ + "cosmos", + "storage", + "workerContainerApp" + ] + }, + "containerRegistry": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('acr-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "containerRegistryName": { + "value": "[variables('containerRegistryName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "roleAssignedManagedIdentityPrincipalIds": { + "value": [ + "[reference('userAssignedIdentity').outputs.principalId.value]" + ] + }, + "enablePrivateEndpoint": { + "value": "[variables('isAILZIntegrated')]" + }, + "privateEndpointSubnetId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').privateEndpointSubnetId), createObject('value', ''))]", + "acrPrivateDnsZoneId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').privateDnsZoneIds.acr), createObject('value', ''))]", + "publicNetworkAccess": "[if(variables('isAILZIntegrated'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "networkRuleBypassOptions": { + "value": "AzureServices" + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "5338415910451274620" + } + }, + "parameters": { + "containerRegistryName": { + "type": "string", + "metadata": { + "description": "Required: Name of the Container Registry" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional: Location for all resources. Default is the resource group location" + } + }, + "sku": { + "type": "string", + "defaultValue": "Standard", + "allowedValues": [ + "Basic", + "Premium", + "Standard" + ], + "metadata": { + "description": "Optional: Container Registry SKU. Default is Standard, Premium required for private endpoints" + } + }, + "adminUserEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional: Admin user enabled. Default is false" + } + }, + "enablePrivateEndpoint": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable private endpoint for AILZ integrated mode" + } + }, + "privateEndpointSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Subnet ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "acrPrivateDnsZoneId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Azure Container Registry Private DNS Zone ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Public network access setting for the Azure Container Registry" + } + }, + "zoneRedundancy": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Zone redundancy setting for the Azure Container Registry" + } + }, + "networkRuleBypassOptions": { + "type": "string", + "defaultValue": "AzureServices", + "allowedValues": [ + "AzureServices", + "None" + ], + "metadata": { + "description": "Network rule bypass options - allows trusted Azure services to access the registry" + } + }, + "roleAssignedManagedIdentityPrincipalIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Managed Identity that will be given access to the Container Registry" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional: Tags for resources" + } + } + }, + "variables": { + "copy": [ + { + "name": "roleAssignmentsAcrPull", + "count": "[length(parameters('roleAssignedManagedIdentityPrincipalIds'))]", + "input": { + "principalId": "[parameters('roleAssignedManagedIdentityPrincipalIds')[copyIndex('roleAssignmentsAcrPull')]]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "AcrPull" + } + }, + { + "name": "roleAssignmentsAcrPush", + "count": "[length(parameters('roleAssignedManagedIdentityPrincipalIds'))]", + "input": { + "principalId": "[parameters('roleAssignedManagedIdentityPrincipalIds')[copyIndex('roleAssignmentsAcrPush')]]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "AcrPush" + } + }, + { + "name": "roleAssignmentsAcrDelete", + "count": "[length(parameters('roleAssignedManagedIdentityPrincipalIds'))]", + "input": { + "principalId": "[parameters('roleAssignedManagedIdentityPrincipalIds')[copyIndex('roleAssignmentsAcrDelete')]]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "AcrDelete" + } + } + ] + }, + "resources": { + "containerRegistry": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('containerRegistry-{0}', uniqueString('containerRegistry', deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('containerRegistryName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "acrSku": "[if(parameters('enablePrivateEndpoint'), createObject('value', 'Premium'), createObject('value', parameters('sku')))]", + "acrAdminUserEnabled": { + "value": "[parameters('adminUserEnabled')]" + }, + "publicNetworkAccess": { + "value": "[parameters('publicNetworkAccess')]" + }, + "zoneRedundancy": { + "value": "[parameters('zoneRedundancy')]" + }, + "networkRuleBypassOptions": { + "value": "[parameters('networkRuleBypassOptions')]" + }, + "roleAssignments": { + "value": "[concat(variables('roleAssignmentsAcrPull'), variables('roleAssignmentsAcrPush'), variables('roleAssignmentsAcrDelete'))]" + }, + "privateEndpoints": "[if(parameters('enablePrivateEndpoint'), createObject('value', createArray(createObject('name', format('{0}-pe', parameters('containerRegistryName')), 'resourceGroupResourceId', resourceGroup().id, 'subnetResourceId', parameters('privateEndpointSubnetId'), 'service', 'registry', 'privateLinkServiceConnectionName', format('{0}-acr-plsc', parameters('containerRegistryName')), 'privateDnsZoneGroups', createArray(createObject('name', 'acr-dns-zone-group', 'privateDnsZoneGroupConfigs', if(not(empty(parameters('acrPrivateDnsZoneId'))), createArray(createObject('name', 'acr-config', 'privateDnsZoneResourceId', parameters('acrPrivateDnsZoneId'))), createArray())))))), createObject('value', createArray()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10440624024470892086" + }, + "name": "Azure Container Registries (ACR)", + "description": "This module deploys an Azure Container Registry (ACR)." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "scopeMapsType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the scope map." + } + }, + "actions": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The list of scoped permissions for registry artifacts." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The user friendly description of the scope map." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a scope map." + } + }, + "cacheRuleType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the cache rule. Will be derived from the source repository name if not defined." + } + }, + "sourceRepository": { + "type": "string", + "metadata": { + "description": "Required. Source repository pulled from upstream." + } + }, + "targetRepository": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Target repository specified in docker pull command. E.g.: docker pull myregistry.azurecr.io/{targetRepository}:{tag}." + } + }, + "credentialSetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the credential store which is associated with the cache rule." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cache rule." + } + }, + "credentialSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the credential set." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityOnlySysAssignedType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "authCredentials": { + "type": "array", + "items": { + "$ref": "#/definitions/authCredentialsType" + }, + "metadata": { + "description": "Required. List of authentication credentials stored for an upstream. Usually consists of a primary and an optional secondary credential." + } + }, + "loginServer": { + "type": "string", + "metadata": { + "description": "Required. The credentials are stored for this upstream or login server." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a credential set." + } + }, + "replicationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the replication." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "regionEndpointEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether the replication regional endpoint is enabled. Requests will not be routed to a replication whose regional endpoint is disabled, however its data will continue to be synced with other replications." + } + }, + "zoneRedundancy": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "nullable": true, + "metadata": { + "description": "Optional. Whether or not zone redundancy is enabled for this container registry." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a replication." + } + }, + "webhookType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Optional. The name of the registry webhook." + } + }, + "serviceUri": { + "type": "string", + "metadata": { + "description": "Required. The service URI for the webhook to post notifications." + } + }, + "status": { + "type": "string", + "allowedValues": [ + "disabled", + "enabled" + ], + "nullable": true, + "metadata": { + "description": "Optional. The status of the webhook at the time the operation was called." + } + }, + "action": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of actions that trigger the webhook to post notifications." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "customHeaders": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Custom headers that will be added to the webhook notifications." + } + }, + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The scope of repositories where the event can be triggered. For example, 'foo:*' means events for all tags under repository 'foo'. 'foo:bar' means events for 'foo:bar' only. 'foo' is equivalent to 'foo:latest'. Empty means all events." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a webhook." + } + }, + "_1.lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "authCredentialsType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the credential." + } + }, + "usernameSecretIdentifier": { + "type": "string", + "metadata": { + "description": "Required. KeyVault Secret URI for accessing the username." + } + }, + "passwordSecretIdentifier": { + "type": "string", + "metadata": { + "description": "Required. KeyVault Secret URI for accessing the password." + } + } + }, + "metadata": { + "description": "The type for auth credentials.", + "__bicep_imported_from!": { + "sourceTemplate": "credential-set/main.bicep" + } + } + }, + "customerManagedKeyWithAutoRotateType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + } + }, + "keyName": { + "type": "string", + "metadata": { + "description": "Required. The name of the customer managed key to use for encryption." + } + }, + "keyVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, using version as per 'autoRotationEnabled' setting." + } + }, + "autoRotationEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable auto-rotating to the latest key version. Default is `true`. If set to `false`, the latest key version at the time of the deployment is used." + } + }, + "userAssignedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type supports auto-rotation of the customer-managed key.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "managedIdentityOnlySysAssignedType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if only system-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateEndpointSingleServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private Endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the Private Endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + } + }, + "lock": { + "$ref": "#/definitions/_1.lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Required. Name of your Azure Container Registry." + } + }, + "acrAdminUserEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable admin user that have push / pull permission to the registry." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "acrSku": { + "type": "string", + "defaultValue": "Premium", + "allowedValues": [ + "Basic", + "Premium", + "Standard" + ], + "metadata": { + "description": "Optional. Tier of your Azure container registry." + } + }, + "exportPolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the export policy is enabled or not." + } + }, + "quarantinePolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the quarantine policy is enabled or not. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "trustPolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the trust policy is enabled or not. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "retentionPolicyStatus": { + "type": "string", + "defaultValue": "enabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the retention policy is enabled or not." + } + }, + "retentionPolicyDays": { + "type": "int", + "defaultValue": 15, + "metadata": { + "description": "Optional. The number of days to retain an untagged manifest after which it gets purged." + } + }, + "azureADAuthenticationAsArmPolicyStatus": { + "type": "string", + "defaultValue": "enabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The value that indicates whether the policy for using ARM audience token for a container registry is enabled or not. Default is enabled." + } + }, + "softDeletePolicyStatus": { + "type": "string", + "defaultValue": "disabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. Soft Delete policy status. Default is disabled." + } + }, + "softDeletePolicyDays": { + "type": "int", + "defaultValue": 7, + "metadata": { + "description": "Optional. The number of days after which a soft-deleted item is permanently deleted." + } + }, + "dataEndpointEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable a single data endpoint per region for serving data. Not relevant in case of disabled public access. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "publicNetworkAccess": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkRuleSetIpRules are not set. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "networkRuleBypassOptions": { + "type": "string", + "defaultValue": "AzureServices", + "allowedValues": [ + "AzureServices", + "None" + ], + "metadata": { + "description": "Optional. Whether to allow trusted Azure services to access a network restricted registry." + } + }, + "networkRuleSetDefaultAction": { + "type": "string", + "defaultValue": "Deny", + "allowedValues": [ + "Allow", + "Deny" + ], + "metadata": { + "description": "Optional. The default action of allow or deny when no other rules match." + } + }, + "networkRuleSetIpRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. The IP ACL rules. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible. Note, requires the 'acrSku' to be 'Premium'." + } + }, + "zoneRedundancy": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Whether or not zone redundancy is enabled for this container registry." + } + }, + "replications": { + "type": "array", + "items": { + "$ref": "#/definitions/replicationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. All replications to create." + } + }, + "webhooks": { + "type": "array", + "items": { + "$ref": "#/definitions/webhookType" + }, + "nullable": true, + "metadata": { + "description": "Optional. All webhooks to create." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.ContainerRegistry/registries@2025-04-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "anonymousPullEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables registry-wide pull from unauthenticated clients. It's in preview and available in the Standard and Premium service tiers." + } + }, + "customerManagedKey": { + "$ref": "#/definitions/customerManagedKeyWithAutoRotateType", + "nullable": true, + "metadata": { + "description": "Optional. The customer managed key definition." + } + }, + "cacheRules": { + "type": "array", + "items": { + "$ref": "#/definitions/cacheRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of Cache Rules." + } + }, + "credentialSets": { + "type": "array", + "items": { + "$ref": "#/definitions/credentialSetType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of Credential Sets." + } + }, + "scopeMaps": { + "type": "array", + "items": { + "$ref": "#/definitions/scopeMapsType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Scope maps setting." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "AcrDelete": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c2f4ef07-c644-48eb-af81-4b1b4947fb11')]", + "AcrImageSigner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '6cef56e8-d556-48e5-a04f-b8e64114680f')]", + "AcrPull": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')]", + "AcrPush": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec')]", + "AcrQuarantineReader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'cdda3590-29a3-44f6-95f2-9f980659eb04')]", + "AcrQuarantineWriter": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c8d4ff99-41c3-41a8-9f60-21dfdad59608')]", + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "cMKKeyVault::cMKKey": { + "condition": "[and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), not(empty(tryGet(parameters('customerManagedKey'), 'keyName')))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.containerregistry-registry.{0}.{1}', replace('0.9.3', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "cMKKeyVault": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId')))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + }, + "cMKUserAssignedIdentity": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + }, + "registry": { + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-06-01-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('acrSku')]" + }, + "properties": { + "anonymousPullEnabled": "[parameters('anonymousPullEnabled')]", + "adminUserEnabled": "[parameters('acrAdminUserEnabled')]", + "encryption": "[if(not(empty(parameters('customerManagedKey'))), createObject('status', 'enabled', 'keyVaultProperties', createObject('identity', if(not(empty(coalesce(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), ''))), reference('cMKUserAssignedIdentity').clientId, null()), 'keyIdentifier', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), format('{0}/{1}', reference('cMKKeyVault::cMKKey').keyUri, tryGet(parameters('customerManagedKey'), 'keyVersion')), if(coalesce(tryGet(parameters('customerManagedKey'), 'autoRotationEnabled'), true()), reference('cMKKeyVault::cMKKey').keyUri, reference('cMKKeyVault::cMKKey').keyUriWithVersion)))), null())]", + "policies": { + "azureADAuthenticationAsArmPolicy": { + "status": "[parameters('azureADAuthenticationAsArmPolicyStatus')]" + }, + "exportPolicy": "[if(equals(parameters('acrSku'), 'Premium'), createObject('status', parameters('exportPolicyStatus')), null())]", + "quarantinePolicy": "[if(equals(parameters('acrSku'), 'Premium'), createObject('status', parameters('quarantinePolicyStatus')), null())]", + "trustPolicy": "[if(equals(parameters('acrSku'), 'Premium'), createObject('type', 'Notary', 'status', parameters('trustPolicyStatus')), null())]", + "retentionPolicy": "[if(equals(parameters('acrSku'), 'Premium'), createObject('days', parameters('retentionPolicyDays'), 'status', parameters('retentionPolicyStatus')), null())]", + "softDeletePolicy": { + "retentionDays": "[parameters('softDeletePolicyDays')]", + "status": "[parameters('softDeletePolicyStatus')]" + } + }, + "dataEndpointEnabled": "[parameters('dataEndpointEnabled')]", + "publicNetworkAccess": "[if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(and(not(empty(parameters('privateEndpoints'))), empty(parameters('networkRuleSetIpRules'))), 'Disabled', null()))]", + "networkRuleBypassOptions": "[parameters('networkRuleBypassOptions')]", + "networkRuleSet": "[if(not(empty(parameters('networkRuleSetIpRules'))), createObject('defaultAction', parameters('networkRuleSetDefaultAction'), 'ipRules', parameters('networkRuleSetIpRules')), null())]", + "zoneRedundancy": "[if(equals(parameters('acrSku'), 'Premium'), parameters('zoneRedundancy'), null())]" + }, + "dependsOn": [ + "cMKKeyVault::cMKKey", + "cMKUserAssignedIdentity" + ] + }, + "registry_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "registry" + ] + }, + "registry_diagnosticSettings": { + "copy": { + "name": "registry_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "registry" + ] + }, + "registry_roleAssignments": { + "copy": { + "name": "registry_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.ContainerRegistry/registries/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "registry" + ] + }, + "registry_scopeMaps": { + "copy": { + "name": "registry_scopeMaps", + "count": "[length(coalesce(parameters('scopeMaps'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Registry-Scope-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(coalesce(parameters('scopeMaps'), createArray())[copyIndex()], 'name')]" + }, + "actions": { + "value": "[coalesce(parameters('scopeMaps'), createArray())[copyIndex()].actions]" + }, + "description": { + "value": "[tryGet(coalesce(parameters('scopeMaps'), createArray())[copyIndex()], 'description')]" + }, + "registryName": { + "value": "[parameters('name')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "6143951528715126111" + }, + "name": "Container Registries scopeMaps", + "description": "This module deploys an Azure Container Registry (ACR) scopeMap." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "[format('{0}-scopemaps', parameters('registryName'))]", + "metadata": { + "description": "Optional. The name of the scope map." + } + }, + "actions": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The list of scoped permissions for registry artifacts." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The user friendly description of the scope map." + } + } + }, + "resources": { + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-06-01-preview", + "name": "[parameters('registryName')]" + }, + "scopeMap": { + "type": "Microsoft.ContainerRegistry/registries/scopeMaps", + "apiVersion": "2023-06-01-preview", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "properties": { + "actions": "[parameters('actions')]", + "description": "[parameters('description')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the scope map." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the scope map was created in." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the scope map." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/scopeMaps', parameters('registryName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_replications": { + "copy": { + "name": "registry_replications", + "count": "[length(coalesce(parameters('replications'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Registry-Replication-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('replications'), createArray())[copyIndex()].name]" + }, + "registryName": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[coalesce(parameters('replications'), createArray())[copyIndex()].location]" + }, + "regionEndpointEnabled": { + "value": "[tryGet(coalesce(parameters('replications'), createArray())[copyIndex()], 'regionEndpointEnabled')]" + }, + "zoneRedundancy": { + "value": "[tryGet(coalesce(parameters('replications'), createArray())[copyIndex()], 'zoneRedundancy')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('replications'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9998680016086915512" + }, + "name": "Azure Container Registry (ACR) Replications", + "description": "This module deploys an Azure Container Registry (ACR) Replication." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the replication." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "regionEndpointEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Specifies whether the replication regional endpoint is enabled. Requests will not be routed to a replication whose regional endpoint is disabled, however its data will continue to be synced with other replications." + } + }, + "zoneRedundancy": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Whether or not zone redundancy is enabled for this container registry." + } + } + }, + "resources": { + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-06-01-preview", + "name": "[parameters('registryName')]" + }, + "replication": { + "type": "Microsoft.ContainerRegistry/registries/replications", + "apiVersion": "2023-06-01-preview", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "regionEndpointEnabled": "[parameters('regionEndpointEnabled')]", + "zoneRedundancy": "[parameters('zoneRedundancy')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the replication." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the replication." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/replications', parameters('registryName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the replication was created in." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('replication', '2023-06-01-preview', 'full').location]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_credentialSets": { + "copy": { + "name": "registry_credentialSets", + "count": "[length(coalesce(parameters('credentialSets'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Registry-CredentialSet-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('credentialSets'), createArray())[copyIndex()].name]" + }, + "registryName": { + "value": "[parameters('name')]" + }, + "managedIdentities": { + "value": "[coalesce(parameters('credentialSets'), createArray())[copyIndex()].managedIdentities]" + }, + "authCredentials": { + "value": "[coalesce(parameters('credentialSets'), createArray())[copyIndex()].authCredentials]" + }, + "loginServer": { + "value": "[coalesce(parameters('credentialSets'), createArray())[copyIndex()].loginServer]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10146775336818580275" + }, + "name": "Container Registries Credential Sets", + "description": "This module deploys an ACR Credential Set." + }, + "definitions": { + "authCredentialsType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the credential." + } + }, + "usernameSecretIdentifier": { + "type": "string", + "metadata": { + "description": "Required. KeyVault Secret URI for accessing the username." + } + }, + "passwordSecretIdentifier": { + "type": "string", + "metadata": { + "description": "Required. KeyVault Secret URI for accessing the password." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for auth credentials." + } + }, + "managedIdentityOnlySysAssignedType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if only system-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the credential set." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityOnlySysAssignedType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "authCredentials": { + "type": "array", + "items": { + "$ref": "#/definitions/authCredentialsType" + }, + "metadata": { + "description": "Required. List of authentication credentials stored for an upstream. Usually consists of a primary and an optional secondary credential." + } + }, + "loginServer": { + "type": "string", + "metadata": { + "description": "Required. The credentials are stored for this upstream or login server." + } + } + }, + "variables": { + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), 'SystemAssigned', null())), null())]" + }, + "resources": { + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-06-01-preview", + "name": "[parameters('registryName')]" + }, + "credentialSet": { + "type": "Microsoft.ContainerRegistry/registries/credentialSets", + "apiVersion": "2023-11-01-preview", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "identity": "[variables('identity')]", + "properties": { + "authCredentials": "[parameters('authCredentials')]", + "loginServer": "[parameters('loginServer')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The Name of the Credential Set." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Credential Set." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Credential Set." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/credentialSets', parameters('registryName'), parameters('name'))]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('credentialSet', '2023-11-01-preview', 'full'), 'identity'), 'principalId')]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_cacheRules": { + "copy": { + "name": "registry_cacheRules", + "count": "[length(coalesce(parameters('cacheRules'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Registry-Cache-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "registryName": { + "value": "[parameters('name')]" + }, + "sourceRepository": { + "value": "[coalesce(parameters('cacheRules'), createArray())[copyIndex()].sourceRepository]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('cacheRules'), createArray())[copyIndex()], 'name')]" + }, + "targetRepository": { + "value": "[coalesce(tryGet(coalesce(parameters('cacheRules'), createArray())[copyIndex()], 'targetRepository'), coalesce(parameters('cacheRules'), createArray())[copyIndex()].sourceRepository)]" + }, + "credentialSetResourceId": { + "value": "[tryGet(coalesce(parameters('cacheRules'), createArray())[copyIndex()], 'credentialSetResourceId')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "16179895563671172347" + }, + "name": "Container Registries Cache", + "description": "Cache for Azure Container Registry (Preview) feature allows users to cache container images in a private container registry. Cache for ACR, is a preview feature available in Basic, Standard, and Premium service tiers ([ref](https://learn.microsoft.com/en-us/azure/container-registry/tutorial-registry-cache))." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "[replace(replace(replace(parameters('sourceRepository'), '/', '-'), '.', '-'), '*', '')]", + "metadata": { + "description": "Optional. The name of the cache rule. Will be derived from the source repository name if not defined." + } + }, + "sourceRepository": { + "type": "string", + "metadata": { + "description": "Required. Source repository pulled from upstream." + } + }, + "targetRepository": { + "type": "string", + "defaultValue": "[parameters('sourceRepository')]", + "metadata": { + "description": "Optional. Target repository specified in docker pull command. E.g.: docker pull myregistry.azurecr.io/{targetRepository}:{tag}." + } + }, + "credentialSetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the credential store which is associated with the cache rule." + } + } + }, + "resources": { + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-06-01-preview", + "name": "[parameters('registryName')]" + }, + "cacheRule": { + "type": "Microsoft.ContainerRegistry/registries/cacheRules", + "apiVersion": "2023-06-01-preview", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "properties": { + "sourceRepository": "[parameters('sourceRepository')]", + "targetRepository": "[parameters('targetRepository')]", + "credentialSetResourceId": "[parameters('credentialSetResourceId')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The Name of the Cache Rule." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Cache Rule." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Cache Rule." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/cacheRules', parameters('registryName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "registry", + "registry_credentialSets" + ] + }, + "registry_webhooks": { + "copy": { + "name": "registry_webhooks", + "count": "[length(coalesce(parameters('webhooks'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Registry-Webhook-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('webhooks'), createArray())[copyIndex()].name]" + }, + "registryName": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'location'), parameters('location'))]" + }, + "action": { + "value": "[tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'action')]" + }, + "customHeaders": { + "value": "[tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'customHeaders')]" + }, + "scope": { + "value": "[tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'scope')]" + }, + "status": { + "value": "[tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'status')]" + }, + "serviceUri": { + "value": "[coalesce(parameters('webhooks'), createArray())[copyIndex()].serviceUri]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('webhooks'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "6514847976022081392" + }, + "name": "Azure Container Registry (ACR) Webhooks", + "description": "This module deploys an Azure Container Registry (ACR) Webhook." + }, + "parameters": { + "registryName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent registry. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "[format('{0}webhook', parameters('registryName'))]", + "minLength": 5, + "maxLength": 50, + "metadata": { + "description": "Optional. The name of the registry webhook." + } + }, + "serviceUri": { + "type": "string", + "metadata": { + "description": "Required. The service URI for the webhook to post notifications." + } + }, + "status": { + "type": "string", + "defaultValue": "enabled", + "allowedValues": [ + "disabled", + "enabled" + ], + "metadata": { + "description": "Optional. The status of the webhook at the time the operation was called." + } + }, + "action": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [ + "chart_delete", + "chart_push", + "delete", + "push", + "quarantine" + ], + "metadata": { + "description": "Optional. The list of actions that trigger the webhook to post notifications." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "customHeaders": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Custom headers that will be added to the webhook notifications." + } + }, + "scope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The scope of repositories where the event can be triggered. For example, 'foo:*' means events for all tags under repository 'foo'. 'foo:bar' means events for 'foo:bar' only. 'foo' is equivalent to 'foo:latest'. Empty means all events." + } + } + }, + "resources": { + "registry": { + "existing": true, + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-06-01-preview", + "name": "[parameters('registryName')]" + }, + "webhook": { + "type": "Microsoft.ContainerRegistry/registries/webhooks", + "apiVersion": "2023-06-01-preview", + "name": "[format('{0}/{1}', parameters('registryName'), parameters('name'))]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "actions": "[parameters('action')]", + "customHeaders": "[parameters('customHeaders')]", + "scope": "[parameters('scope')]", + "serviceUri": "[parameters('serviceUri')]", + "status": "[parameters('status')]" + } + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the webhook." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries/webhooks', parameters('registryName'), parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the webhook." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Azure container registry." + }, + "value": "[resourceGroup().name]" + }, + "actions": { + "type": "array", + "metadata": { + "description": "The actions of the webhook." + }, + "value": "[reference('webhook').actions]" + }, + "status": { + "type": "string", + "metadata": { + "description": "The status of the webhook." + }, + "value": "[reference('webhook').status]" + }, + "provistioningState": { + "type": "string", + "metadata": { + "description": "The provisioning state of the webhook." + }, + "value": "[reference('webhook').provisioningState]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('webhook', '2023-06-01-preview', 'full').location]" + } + } + } + }, + "dependsOn": [ + "registry" + ] + }, + "registry_privateEndpoints": { + "copy": { + "name": "registry_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-registry-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.ContainerRegistry/registries', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'registry')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "registry", + "registry_replications" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The Name of the Azure container registry." + }, + "value": "[parameters('name')]" + }, + "loginServer": { + "type": "string", + "metadata": { + "description": "The reference to the Azure container registry." + }, + "value": "[reference('registry').loginServer]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the Azure container registry." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Azure container registry." + }, + "value": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('name'))]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('registry', '2023-06-01-preview', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('registry', '2023-06-01-preview', 'full').location]" + }, + "credentialSetsSystemAssignedMIPrincipalIds": { + "type": "array", + "metadata": { + "description": "The Principal IDs of the ACR Credential Sets system-assigned identities." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('credentialSets'), createArray()))))]", + "input": "[tryGet(tryGet(reference(format('registry_credentialSets[{0}]', range(0, length(coalesce(parameters('credentialSets'), createArray())))[copyIndex()])).outputs, 'systemAssignedMIPrincipalId'), 'value')]" + } + }, + "credentialSetsResourceIds": { + "type": "array", + "metadata": { + "description": "The Resource IDs of the ACR Credential Sets." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('credentialSets'), createArray()))))]", + "input": "[reference(format('registry_credentialSets[{0}]', range(0, length(coalesce(parameters('credentialSets'), createArray())))[copyIndex()])).outputs.resourceId.value]" + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the Azure container registry." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('registry_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "value": "[reference('containerRegistry').outputs.name.value]" + }, + "loginServer": { + "type": "string", + "value": "[reference('containerRegistry').outputs.loginServer.value]" + }, + "resourceGroupName": { + "type": "string", + "value": "[reference('containerRegistry').outputs.resourceGroupName.value]" + }, + "resourceId": { + "type": "string", + "value": "[reference('containerRegistry').outputs.resourceId.value]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "value": "[tryGet(tryGet(reference('containerRegistry').outputs, 'systemAssignedMIPrincipalId'), 'value')]" + }, + "credentialSetsSystemAssignedMIPrincipalIds": { + "type": "array", + "value": "[reference('containerRegistry').outputs.credentialSetsSystemAssignedMIPrincipalIds.value]" + }, + "credentialSetsResourceIds": { + "type": "array", + "value": "[reference('containerRegistry').outputs.credentialSetsResourceIds.value]" + }, + "privateEndpoints": { + "type": "array", + "value": "[reference('containerRegistry').outputs.privateEndpoints.value]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "containerAppsEnvironment": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('cae-la-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "containerAppsEnvironmentName": { + "value": "[variables('containerAppsEnvironmentName')]" + }, + "logAnalyticsWorkspaceId": "[if(not(empty(parameters('existingLogAnalyticsWorkspaceId'))), createObject('value', reference(parameters('existingLogAnalyticsWorkspaceId'), '2021-12-01-preview').customerId), createObject('value', reference('logAnalytics').outputs.logAnalyticsWorkspaceId.value))]", + "logAnalyticsPrimarySharedKey": "[if(not(empty(parameters('existingLogAnalyticsWorkspaceId'))), createObject('value', listKeys(parameters('existingLogAnalyticsWorkspaceId'), '2021-12-01-preview').primarySharedKey), createObject('value', listOutputsWithSecureValues('logAnalytics', '2025-04-01').primarySharedKey))]", + "userAssignedResourceIds": { + "value": [ + "[reference('userAssignedIdentity').outputs.resourceId.value]" + ] + }, + "location": { + "value": "[parameters('location')]" + }, + "enablePrivateEndpoint": { + "value": "[variables('isAILZIntegrated')]" + }, + "privateEndpointSubnetId": "[if(variables('isAILZIntegrated'), createObject('value', variables('networkConfig').containerAppsSubnetId), createObject('value', null()))]", + "publicNetworkAccess": "[if(variables('isAILZIntegrated'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "12424513027333998147" + } + }, + "parameters": { + "containerAppsEnvironmentName": { + "type": "string", + "metadata": { + "description": "Name of the Container Apps Environment" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources" + } + }, + "logAnalyticsWorkspaceId": { + "type": "string", + "metadata": { + "description": "Log Analytics workspace id output from log-analytics-ws.bicep module" + } + }, + "logAnalyticsPrimarySharedKey": { + "type": "securestring", + "metadata": { + "description": "Log Analytics workspace primary shared key output from log-analytics-ws.bicep module" + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "User Assigned Identity resource IDs that will be assigned to the Container Apps Environment" + } + }, + "enablePrivateEndpoint": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Enable private endpoint for AILZ integrated mode" + } + }, + "privateEndpointSubnetId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Subnet ID for private endpoint (required when enablePrivateEndpoint is true)" + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Public network access setting for the Azure Container Registry" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for resources" + } + } + }, + "resources": { + "containerAppsEnvironment": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}.containerAppsEnvironment', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('containerAppsEnvironmentName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "zoneRedundant": { + "value": false + }, + "appLogsConfiguration": { + "value": { + "destination": "log-analytics", + "logAnalyticsConfiguration": { + "customerId": "[parameters('logAnalyticsWorkspaceId')]", + "sharedKey": "[parameters('logAnalyticsPrimarySharedKey')]" + } + } + }, + "workloadProfiles": { + "value": [ + { + "name": "Consumption", + "workloadProfileType": "Consumption" + } + ] + }, + "platformReservedCidr": { + "value": "172.17.17.0/24" + }, + "platformReservedDnsIP": { + "value": "172.17.17.17" + }, + "publicNetworkAccess": { + "value": "[parameters('publicNetworkAccess')]" + }, + "internal": { + "value": "[parameters('enablePrivateEndpoint')]" + }, + "infrastructureSubnetResourceId": "[if(parameters('enablePrivateEndpoint'), createObject('value', parameters('privateEndpointSubnetId')), createObject('value', null()))]", + "managedIdentities": { + "value": { + "systemAssigned": true, + "userAssignedResourceIds": "[parameters('userAssignedResourceIds')]" + } + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "1345160196550942789" + }, + "name": "App ManagedEnvironments", + "description": "This module deploys an App Managed Environment (also known as a Container App Environment)." + }, + "definitions": { + "certificateType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the certificate." + } + }, + "certificateType": { + "type": "string", + "allowedValues": [ + "ImagePullTrustedCA", + "ServerSSLCertificate" + ], + "nullable": true, + "metadata": { + "description": "Optional. The type of the certificate." + } + }, + "certificateValue": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The value of the certificate. PFX or PEM blob." + } + }, + "certificatePassword": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The password of the certificate." + } + }, + "certificateKeyVaultProperties": { + "$ref": "#/definitions/certificateKeyVaultPropertiesType", + "nullable": true, + "metadata": { + "description": "Optional. A key vault reference." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a certificate." + } + }, + "storageType": { + "type": "object", + "properties": { + "accessMode": { + "type": "string", + "allowedValues": [ + "ReadOnly", + "ReadWrite" + ], + "metadata": { + "description": "Required. Access mode for storage: \"ReadOnly\" or \"ReadWrite\"." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "NFS", + "SMB" + ], + "metadata": { + "description": "Required. Type of storage: \"SMB\" or \"NFS\"." + } + }, + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Required. Storage account name." + } + }, + "shareName": { + "type": "string", + "metadata": { + "description": "Required. File share name." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of the storage." + } + }, + "appLogsConfigurationType": { + "type": "object", + "properties": { + "destination": { + "type": "string", + "allowedValues": [ + "azure-monitor", + "log-analytics", + "none" + ], + "nullable": true, + "metadata": { + "description": "Optional. The destination of the logs." + } + }, + "logAnalyticsConfiguration": { + "type": "object", + "properties": { + "customerId": { + "type": "string", + "metadata": { + "description": "Required. The Log Analytics Workspace ID." + } + }, + "sharedKey": { + "type": "securestring", + "metadata": { + "description": "Required. The shared key of the Log Analytics workspace." + } + } + }, + "nullable": true, + "metadata": { + "description": "Conditional. The Log Analytics configuration. Required if `destination` is `log-analytics`." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the App Logs Configuration." + } + }, + "certificateKeyVaultPropertiesType": { + "type": "object", + "properties": { + "identityResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the identity. This is the identity that will be used to access the key vault." + } + }, + "keyVaultUrl": { + "type": "string", + "metadata": { + "description": "Required. A key vault URL referencing the wildcard certificate that will be used for the custom domain." + } + } + }, + "metadata": { + "description": "The type for the certificate's key vault properties.", + "__bicep_imported_from!": { + "sourceTemplate": "certificates/main.bicep" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Container Apps Managed Environment." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/managedEnvironments@2024-10-02-preview#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "appInsightsConnectionString": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Application Insights connection string." + } + }, + "daprAIConnectionString": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Application Insights connection string used by Dapr to export Service to Service communication telemetry." + } + }, + "daprAIInstrumentationKey": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Azure Monitor instrumentation key used by Dapr to export Service to Service communication telemetry." + } + }, + "dockerBridgeCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Conditional. CIDR notation IP range assigned to the Docker bridge, network. It must not overlap with any other provided IP ranges and can only be used when the environment is deployed into a virtual network. If not provided, it will be set with a default value by the platform. Required if zoneRedundant is set to true to make the resource WAF compliant." + } + }, + "infrastructureSubnetResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Conditional. Resource ID of a subnet for infrastructure components. This is used to deploy the environment into a virtual network. Must not overlap with any other provided IP ranges. Required if \"internal\" is set to true. Required if zoneRedundant is set to true to make the resource WAF compliant." + } + }, + "internal": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Conditional. Boolean indicating the environment only has an internal load balancer. These environments do not have a public static IP resource. If set to true, then \"infrastructureSubnetId\" must be provided. Required if zoneRedundant is set to true to make the resource WAF compliant." + } + }, + "platformReservedCidr": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Conditional. IP range in CIDR notation that can be reserved for environment infrastructure IP addresses. It must not overlap with any other provided IP ranges and can only be used when the environment is deployed into a virtual network. If not provided, it will be set with a default value by the platform. Required if zoneRedundant is set to true to make the resource WAF compliant." + } + }, + "platformReservedDnsIP": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Conditional. An IP address from the IP range defined by \"platformReservedCidr\" that will be reserved for the internal DNS server. It must not be the first address in the range and can only be used when the environment is deployed into a virtual network. If not provided, it will be set with a default value by the platform. Required if zoneRedundant is set to true to make the resource WAF compliant." + } + }, + "peerTrafficEncryption": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Whether or not to encrypt peer traffic." + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether to allow or block all public traffic." + } + }, + "zoneRedundant": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Whether or not this Managed Environment is zone-redundant." + } + }, + "certificatePassword": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Password of the certificate used by the custom domain." + } + }, + "certificateValue": { + "type": "securestring", + "defaultValue": "", + "metadata": { + "description": "Optional. Certificate to use for the custom domain. PFX or PEM." + } + }, + "dnsSuffix": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. DNS suffix for the environment domain." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "openTelemetryConfiguration": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Open Telemetry configuration." + } + }, + "workloadProfiles": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Conditional. Workload profiles configured for the Managed Environment. Required if zoneRedundant is set to true to make the resource WAF compliant." + } + }, + "infrastructureResourceGroupName": { + "type": "string", + "defaultValue": "[take(format('ME_{0}', parameters('name')), 63)]", + "metadata": { + "description": "Conditional. Name of the infrastructure resource group. If not provided, it will be set with a default value. Required if zoneRedundant is set to true to make the resource WAF compliant." + } + }, + "storages": { + "type": "array", + "items": { + "$ref": "#/definitions/storageType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The list of storages to mount on the environment." + } + }, + "certificate": { + "$ref": "#/definitions/certificateType", + "nullable": true, + "metadata": { + "description": "Optional. A Managed Environment Certificate." + } + }, + "appLogsConfiguration": { + "$ref": "#/definitions/appLogsConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. The AppLogsConfiguration for the Managed Environment." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "managedEnvironment::storage": { + "copy": { + "name": "managedEnvironment::storage", + "count": "[length(coalesce(parameters('storages'), createArray()))]" + }, + "type": "Microsoft.App/managedEnvironments/storages", + "apiVersion": "2024-10-02-preview", + "name": "[format('{0}/{1}', parameters('name'), coalesce(parameters('storages'), createArray())[copyIndex()].shareName)]", + "properties": { + "nfsAzureFile": "[if(equals(coalesce(parameters('storages'), createArray())[copyIndex()].kind, 'NFS'), createObject('accessMode', coalesce(parameters('storages'), createArray())[copyIndex()].accessMode, 'server', format('{0}.file.{1}', coalesce(parameters('storages'), createArray())[copyIndex()].storageAccountName, environment().suffixes.storage), 'shareName', format('/{0}/{1}', coalesce(parameters('storages'), createArray())[copyIndex()].storageAccountName, coalesce(parameters('storages'), createArray())[copyIndex()].shareName)), null())]", + "azureFile": "[if(equals(coalesce(parameters('storages'), createArray())[copyIndex()].kind, 'SMB'), createObject('accessMode', coalesce(parameters('storages'), createArray())[copyIndex()].accessMode, 'accountName', coalesce(parameters('storages'), createArray())[copyIndex()].storageAccountName, 'accountKey', listkeys(resourceId('Microsoft.Storage/storageAccounts', coalesce(parameters('storages'), createArray())[copyIndex()].storageAccountName), '2023-01-01').keys[0].value, 'shareName', coalesce(parameters('storages'), createArray())[copyIndex()].shareName), null())]" + }, + "dependsOn": [ + "managedEnvironment" + ] + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-11-01", + "name": "[format('46d3xbcp.res.app-managedenvironment.{0}.{1}', replace('0.11.3', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "managedEnvironment": { + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2024-10-02-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", + "properties": { + "appInsightsConfiguration": { + "connectionString": "[parameters('appInsightsConnectionString')]" + }, + "appLogsConfiguration": "[parameters('appLogsConfiguration')]", + "daprAIConnectionString": "[parameters('daprAIConnectionString')]", + "daprAIInstrumentationKey": "[parameters('daprAIInstrumentationKey')]", + "customDomainConfiguration": { + "certificatePassword": "[parameters('certificatePassword')]", + "certificateValue": "[if(not(empty(parameters('certificateValue'))), parameters('certificateValue'), null())]", + "dnsSuffix": "[parameters('dnsSuffix')]", + "certificateKeyVaultProperties": "[if(not(empty(tryGet(parameters('certificate'), 'certificateKeyVaultProperties'))), createObject('identity', tryGet(parameters('certificate'), 'certificateKeyVaultProperties', 'identityResourceId'), 'keyVaultUrl', tryGet(parameters('certificate'), 'certificateKeyVaultProperties', 'keyVaultUrl')), null())]" + }, + "openTelemetryConfiguration": "[if(not(empty(parameters('openTelemetryConfiguration'))), parameters('openTelemetryConfiguration'), null())]", + "peerTrafficConfiguration": { + "encryption": { + "enabled": "[parameters('peerTrafficEncryption')]" + } + }, + "publicNetworkAccess": "[parameters('publicNetworkAccess')]", + "vnetConfiguration": { + "internal": "[parameters('internal')]", + "infrastructureSubnetId": "[if(not(empty(parameters('infrastructureSubnetResourceId'))), parameters('infrastructureSubnetResourceId'), null())]", + "dockerBridgeCidr": "[if(not(empty(parameters('infrastructureSubnetResourceId'))), parameters('dockerBridgeCidr'), null())]", + "platformReservedCidr": "[if(and(empty(parameters('workloadProfiles')), not(empty(parameters('infrastructureSubnetResourceId')))), parameters('platformReservedCidr'), null())]", + "platformReservedDnsIP": "[if(and(empty(parameters('workloadProfiles')), not(empty(parameters('infrastructureSubnetResourceId')))), parameters('platformReservedDnsIP'), null())]" + }, + "workloadProfiles": "[if(not(empty(parameters('workloadProfiles'))), parameters('workloadProfiles'), null())]", + "zoneRedundant": "[parameters('zoneRedundant')]", + "infrastructureResourceGroup": "[parameters('infrastructureResourceGroupName')]" + } + }, + "managedEnvironment_roleAssignments": { + "copy": { + "name": "managedEnvironment_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.App/managedEnvironments/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.App/managedEnvironments', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "managedEnvironment" + ] + }, + "managedEnvironment_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.App/managedEnvironments/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "managedEnvironment" + ] + }, + "managedEnvironment_certificate": { + "condition": "[not(empty(parameters('certificate')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Managed-Environment-Certificate', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(parameters('certificate'), 'name'), format('cert-{0}', parameters('name')))]" + }, + "managedEnvironmentName": { + "value": "[parameters('name')]" + }, + "certificateKeyVaultProperties": { + "value": "[tryGet(parameters('certificate'), 'certificateKeyVaultProperties')]" + }, + "certificateType": { + "value": "[tryGet(parameters('certificate'), 'certificateType')]" + }, + "certificateValue": { + "value": "[tryGet(parameters('certificate'), 'certificateValue')]" + }, + "certificatePassword": { + "value": "[tryGet(parameters('certificate'), 'certificatePassword')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13507794255589178049" + }, + "name": "App ManagedEnvironments Certificates", + "description": "This module deploys a App Managed Environment Certificate." + }, + "definitions": { + "certificateKeyVaultPropertiesType": { + "type": "object", + "properties": { + "identityResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the identity. This is the identity that will be used to access the key vault." + } + }, + "keyVaultUrl": { + "type": "string", + "metadata": { + "description": "Required. A key vault URL referencing the wildcard certificate that will be used for the custom domain." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the certificate's key vault properties." + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Container Apps Managed Environment Certificate." + } + }, + "managedEnvironmentName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent app managed environment. Required if the template is used in a standalone deployment." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "certificateKeyVaultProperties": { + "$ref": "#/definitions/certificateKeyVaultPropertiesType", + "nullable": true, + "metadata": { + "description": "Optional. A key vault reference to the certificate to use for the custom domain." + } + }, + "certificateType": { + "type": "string", + "nullable": true, + "allowedValues": [ + "ServerSSLCertificate", + "ImagePullTrustedCA" + ], + "metadata": { + "description": "Optional. The type of the certificate." + } + }, + "certificateValue": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The value of the certificate. PFX or PEM blob." + } + }, + "certificatePassword": { + "type": "securestring", + "nullable": true, + "metadata": { + "description": "Optional. The password of the certificate." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + } + }, + "resources": { + "managedEnvironment": { + "existing": true, + "type": "Microsoft.App/managedEnvironments", + "apiVersion": "2024-10-02-preview", + "name": "[parameters('managedEnvironmentName')]" + }, + "managedEnvironmentCertificate": { + "type": "Microsoft.App/managedEnvironments/certificates", + "apiVersion": "2024-10-02-preview", + "name": "[format('{0}/{1}', parameters('managedEnvironmentName'), parameters('name'))]", + "location": "[parameters('location')]", + "properties": { + "certificateKeyVaultProperties": "[if(not(empty(parameters('certificateKeyVaultProperties'))), createObject('identity', parameters('certificateKeyVaultProperties').identityResourceId, 'keyVaultUrl', parameters('certificateKeyVaultProperties').keyVaultUrl), null())]", + "certificateType": "[parameters('certificateType')]", + "password": "[parameters('certificatePassword')]", + "value": "[parameters('certificateValue')]" + }, + "tags": "[parameters('tags')]" + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the key values." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the key values." + }, + "value": "[resourceId('Microsoft.App/managedEnvironments/certificates', parameters('managedEnvironmentName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the batch account was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "managedEnvironment" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Managed Environment was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('managedEnvironment', '2024-10-02-preview', 'full').location]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the Managed Environment." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Managed Environment." + }, + "value": "[resourceId('Microsoft.App/managedEnvironments', parameters('name'))]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('managedEnvironment', '2024-10-02-preview', 'full'), 'identity'), 'principalId')]" + }, + "defaultDomain": { + "type": "string", + "metadata": { + "description": "The Default domain of the Managed Environment." + }, + "value": "[reference('managedEnvironment').defaultDomain]" + }, + "staticIp": { + "type": "string", + "metadata": { + "description": "The IP address of the Managed Environment." + }, + "value": "[reference('managedEnvironment').staticIp]" + }, + "domainVerificationId": { + "type": "string", + "metadata": { + "description": "The domain verification id for custom domains." + }, + "value": "[reference('managedEnvironment').customDomainConfiguration.customDomainVerificationId]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "value": "[reference('containerAppsEnvironment').outputs.name.value]" + }, + "resourceId": { + "type": "string", + "value": "[reference('containerAppsEnvironment').outputs.resourceId.value]" + }, + "location": { + "type": "string", + "value": "[reference('containerAppsEnvironment').outputs.location.value]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "value": "[tryGet(tryGet(reference('containerAppsEnvironment').outputs, 'systemAssignedMIPrincipalId'), 'value')]" + }, + "defaultDomain": { + "type": "string", + "value": "[reference('containerAppsEnvironment').outputs.defaultDomain.value]" + }, + "staticIp": { + "type": "string", + "value": "[reference('containerAppsEnvironment').outputs.staticIp.value]" + }, + "domainVerificationId": { + "type": "string", + "value": "[reference('containerAppsEnvironment').outputs.domainVerificationId.value]" + } + } + } + }, + "dependsOn": [ + "logAnalytics", + "userAssignedIdentity" + ] + }, + "aiFoundry": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('aiFoundry-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiFoundryBaseName": { + "value": "[substring(format('aif{0}', variables('resourceToken')), 0, 12)]" + }, + "roleAssignedManagedIdentityPrincipalIds": { + "value": [ + "[reference('userAssignedIdentity').outputs.principalId.value]" + ] + }, + "location": { + "value": "[parameters('foundryLocation')]" + }, + "tags": { + "value": "[variables('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "9713004289481792639" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional: Location for all resources. Default is the resource group location" + } + }, + "aiFoundryBaseName": { + "type": "string", + "metadata": { + "description": "Required: The AI Foundry base name (max 12 characters)" + } + }, + "roleAssignedManagedIdentityPrincipalIds": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [], + "metadata": { + "description": "Managed Identity that will be given access to the AI Foundry Resource" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags for resources" + } + } + }, + "variables": { + "uniqueRoleAssignmentManagedIdentities": "[union(parameters('roleAssignedManagedIdentityPrincipalIds'), parameters('roleAssignedManagedIdentityPrincipalIds'))]" + }, + "resources": { + "aiFoundry": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('aiFoundry-{0}', uniqueString('aiFoundry', deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "baseName": { + "value": "[parameters('aiFoundryBaseName')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "aiFoundryConfiguration": { + "value": { + "copy": [ + { + "name": "roleAssignments", + "count": "[length(variables('uniqueRoleAssignmentManagedIdentities'))]", + "input": { + "principalId": "[variables('uniqueRoleAssignmentManagedIdentities')[copyIndex('roleAssignments')]]", + "principalType": "ServicePrincipal", + "roleDefinitionIdOrName": "53ca6127-db72-4b80-b1b0-d745d6d5456d" + } + } + ], + "allowProjectManagement": true, + "createCapabilityHosts": false, + "disableLocalAuth": true, + "location": "[parameters('location')]", + "project": { + "desc": "AI Foundry project for ContentFlow Solution Accelerator", + "displayName": "ContentFlow", + "name": "contentflow-project" + }, + "sku": "S0" + } + }, + "aiModelDeployments": { + "value": [ + { + "model": { + "format": "OpenAI", + "name": "gpt-4.1-mini", + "version": "2025-04-14" + }, + "name": "gpt-4.1-mini", + "sku": { + "capacity": 100, + "name": "GlobalStandard" + } + }, + { + "model": { + "format": "OpenAI", + "name": "gpt-4.1", + "version": "2025-04-14" + }, + "name": "gpt-4.1", + "sku": { + "capacity": 100, + "name": "GlobalStandard" + } + } + ] + }, + "includeAssociatedResources": { + "value": false + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "16692041582792913299" + }, + "name": "ai-foundry", + "description": "Creates an AI Foundry account and project with Standard Agent Services." + }, + "definitions": { + "resourceConfigurationType": { + "type": "object", + "properties": { + "existingResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of an existing resource to use instead of creating a new one. If provided, other parameters are ignored." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name to be used when creating the resource. This is ignored if an existingResourceId is provided." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource ID of the Private DNS Zone that associates with the resource. This is required to establish a Private Endpoint and when 'privateEndpointSubnetResourceId' is provided." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Role assignments to apply to the resource when creating it. This is ignored if an existingResourceId is provided." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Custom configuration for a resource, including optional name, existing resource ID, and role assignments." + } + }, + "storageAccountConfigurationType": { + "type": "object", + "properties": { + "existingResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource Id of an existing Storage Account to use instead of creating a new one. If provided, other parameters are ignored." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name to be used when creating the Storage Account. This is ignored if an existingResourceId is provided." + } + }, + "blobPrivateDnsZoneResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource ID of the DNS zone \"blob\" for the Azure Storage Account. This is required to establish a Private Endpoint and when 'privateEndpointSubnetResourceId' is provided." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Role assignments to apply to the resource when creating it. This is ignored if an existingResourceId is provided." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Custom configuration for a Storage Account, including optional name, existing resource ID, containers, and role assignments." + } + }, + "foundryConfigurationType": { + "type": "object", + "properties": { + "accountName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the AI Foundry account." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location of the AI Foundry account. Will default to the resource group location if not specified." + } + }, + "sku": { + "type": "string", + "allowedValues": [ + "C2", + "C3", + "C4", + "DC0", + "F0", + "F1", + "S", + "S0", + "S1", + "S10", + "S2", + "S3", + "S4", + "S5", + "S6", + "S7", + "S8", + "S9" + ], + "nullable": true, + "metadata": { + "description": "Optional. SKU of the AI Foundry / Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region. Defaults to 'S0'." + } + }, + "createCapabilityHosts": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Whether to create Capability Hosts for the AI Agent Service. If true, the AI Foundry Account and default Project will be created with the capability host for the associated resources. Can only be true if 'includeAssociatedResources' is true. Defaults to false." + } + }, + "disableLocalAuth": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Allow only Azure AD authentication. Should be enabled for security reasons. Defaults to true." + } + }, + "allowProjectManagement": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Whether to allow project management in the AI Foundry account. If true, users can create and manage projects within the AI Foundry account. Defaults to true." + } + }, + "networking": { + "$ref": "#/definitions/foundryNetworkConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Values to establish private networking for the AI Foundry account and project." + } + }, + "project": { + "$ref": "#/definitions/foundryProjectConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. AI Foundry default project." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Role assignments to apply to the AI Foundry resource when creating it." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Custom configuration for a AI Foundry, including optional account name and project configuration." + } + }, + "foundryNetworkConfigurationType": { + "type": "object", + "properties": { + "agentServiceSubnetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource ID of the subnet for the Azure AI Services account. This is required if 'createAIAgentService' is true." + } + }, + "cognitiveServicesPrivateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The Resource ID of the Private DNS Zone for the Azure AI Services account." + } + }, + "openAiPrivateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The Resource ID of the Private DNS Zone for the OpenAI account." + } + }, + "aiServicesPrivateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The Resource ID of the Private DNS Zone for the Azure AI Services account." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Values to establish private networking for the AI Foundry service." + } + }, + "foundryProjectConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the AI Foundry project." + } + }, + "displayName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The friendly/display name of the AI Foundry project." + } + }, + "desc": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the AI Foundry project." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Custom configuration for an AI Foundry project, including optional name, friendly name, and description." + } + }, + "deploymentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of cognitive service account deployment." + } + }, + "model": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of Cognitive Services account deployment model." + } + }, + "format": { + "type": "string", + "metadata": { + "description": "Required. The format of Cognitive Services account deployment model." + } + }, + "version": { + "type": "string", + "metadata": { + "description": "Required. The version of Cognitive Services account deployment model." + } + } + }, + "metadata": { + "description": "Required. Properties of Cognitive Services account deployment model." + } + }, + "sku": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource model definition representing SKU." + } + }, + "capacity": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The capacity of the resource model definition representing SKU." + } + }, + "tier": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The tier of the resource model definition representing SKU." + } + }, + "size": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The size of the resource model definition representing SKU." + } + }, + "family": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The family of the resource model definition representing SKU." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource model definition representing SKU." + } + }, + "raiPolicyName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of RAI policy." + } + }, + "versionUpgradeOption": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version upgrade option." + } + } + }, + "metadata": { + "description": "The type for a cognitive services account deployment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/cognitive-services/account:0.12.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "baseName": { + "type": "string", + "minLength": 3, + "maxLength": 12, + "metadata": { + "description": "Required. A friendly application/environment name to serve as the \"base\" when using the default naming for all resources in this deployment." + } + }, + "baseUniqueName": { + "type": "string", + "defaultValue": "[substring(uniqueString(subscription().id, resourceGroup().name, parameters('baseName')), 0, 5)]", + "maxLength": 5, + "metadata": { + "description": "Optional. A unique text value for the application/environment. This is used to ensure resource names are unique for global resources. Defaults to a 5-character substring of the unique string generated from the subscription ID, resource group name, and base name." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources. Defaults to the location of the resource group." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "aiModelDeployments": { + "type": "array", + "items": { + "$ref": "#/definitions/deploymentType" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. Specifies the OpenAI deployments to create." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Resources/resourceGroups@2025-04-01#properties/tags" + }, + "description": "Optional. Specifies the resource tags for all the resources." + }, + "defaultValue": {} + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the AI resources." + } + }, + "includeAssociatedResources": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Whether to include associated resources: Key Vault, AI Search, Storage Account, and Cosmos DB. If true, these resources will be created. Optionally, existing resources of these types can be supplied in their respective parameters. Defaults to false." + } + }, + "privateEndpointSubnetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource ID of the subnet to establish Private Endpoint(s). If provided, private endpoints will be created for the AI Foundry account and associated resources when creating those resource. Each resource will also require supplied private DNS zone resource ID(s) to establish those private endpoints." + } + }, + "aiFoundryConfiguration": { + "$ref": "#/definitions/foundryConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Custom configuration for the AI Foundry." + } + }, + "keyVaultConfiguration": { + "$ref": "#/definitions/resourceConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Custom configuration for the Key Vault." + } + }, + "aiSearchConfiguration": { + "$ref": "#/definitions/resourceConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Custom configuration for the AI Search resource." + } + }, + "storageAccountConfiguration": { + "$ref": "#/definitions/storageAccountConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Custom configuration for the Storage Account." + } + }, + "cosmosDbConfiguration": { + "$ref": "#/definitions/resourceConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Custom configuration for the Cosmos DB Account." + } + } + }, + "variables": { + "resourcesName": "[toLower(trim(replace(replace(replace(replace(replace(replace(format('{0}{1}', parameters('baseName'), parameters('baseUniqueName')), '-', ''), '_', ''), '.', ''), '/', ''), ' ', ''), '*', '')))]", + "projectName": "[if(not(empty(tryGet(tryGet(parameters('aiFoundryConfiguration'), 'project'), 'name'))), parameters('aiFoundryConfiguration').project.name, format('proj-{0}', variables('resourcesName')))]", + "createCapabilityHosts": "[and(coalesce(tryGet(parameters('aiFoundryConfiguration'), 'createCapabilityHosts'), false()), parameters('includeAssociatedResources'))]" + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.ptn.aiml-aifoundry.{0}.{1}', replace('0.5.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "foundryAccount": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.account.{0}', variables('resourcesName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": "[if(not(empty(tryGet(parameters('aiFoundryConfiguration'), 'accountName'))), createObject('value', parameters('aiFoundryConfiguration').accountName), createObject('value', format('ai{0}', variables('resourcesName'))))]", + "location": "[if(not(empty(tryGet(parameters('aiFoundryConfiguration'), 'location'))), createObject('value', parameters('aiFoundryConfiguration').location), createObject('value', parameters('location')))]", + "sku": "[if(not(empty(tryGet(parameters('aiFoundryConfiguration'), 'sku'))), createObject('value', parameters('aiFoundryConfiguration').sku), createObject('value', 'S0'))]", + "disableLocalAuth": { + "value": "[coalesce(tryGet(parameters('aiFoundryConfiguration'), 'disableLocalAuth'), true())]" + }, + "allowProjectManagement": { + "value": "[coalesce(tryGet(parameters('aiFoundryConfiguration'), 'allowProjectManagement'), true())]" + }, + "aiModelDeployments": { + "value": "[parameters('aiModelDeployments')]" + }, + "privateEndpointSubnetResourceId": { + "value": "[parameters('privateEndpointSubnetResourceId')]" + }, + "agentSubnetResourceId": { + "value": "[tryGet(tryGet(parameters('aiFoundryConfiguration'), 'networking'), 'agentServiceSubnetResourceId')]" + }, + "privateDnsZoneResourceIds": "[if(and(not(empty(parameters('privateEndpointSubnetResourceId'))), not(empty(tryGet(parameters('aiFoundryConfiguration'), 'networking')))), createObject('value', createArray(parameters('aiFoundryConfiguration').networking.cognitiveServicesPrivateDnsZoneResourceId, parameters('aiFoundryConfiguration').networking.openAiPrivateDnsZoneResourceId, parameters('aiFoundryConfiguration').networking.aiServicesPrivateDnsZoneResourceId)), createObject('value', createArray()))]", + "roleAssignments": { + "value": "[tryGet(parameters('aiFoundryConfiguration'), 'roleAssignments')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "lock": { + "value": "[parameters('lock')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "11906964530752207086" + } + }, + "definitions": { + "deploymentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of cognitive service account deployment." + } + }, + "model": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of Cognitive Services account deployment model." + } + }, + "format": { + "type": "string", + "metadata": { + "description": "Required. The format of Cognitive Services account deployment model." + } + }, + "version": { + "type": "string", + "metadata": { + "description": "Required. The version of Cognitive Services account deployment model." + } + } + }, + "metadata": { + "description": "Required. Properties of Cognitive Services account deployment model." + } + }, + "sku": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource model definition representing SKU." + } + }, + "capacity": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The capacity of the resource model definition representing SKU." + } + }, + "tier": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The tier of the resource model definition representing SKU." + } + }, + "size": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The size of the resource model definition representing SKU." + } + }, + "family": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The family of the resource model definition representing SKU." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource model definition representing SKU." + } + }, + "raiPolicyName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of RAI policy." + } + }, + "versionUpgradeOption": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version upgrade option." + } + } + }, + "metadata": { + "description": "The type for a cognitive services account deployment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/res/cognitive-services/account:0.12.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the AI Foundry resource." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Required. The location for the AI Foundry resource." + } + }, + "sku": { + "type": "string", + "defaultValue": "S0", + "allowedValues": [ + "C2", + "C3", + "C4", + "F0", + "F1", + "S", + "S0", + "S1", + "S10", + "S2", + "S3", + "S4", + "S5", + "S6", + "S7", + "S8", + "S9", + "DC0" + ], + "metadata": { + "description": "Optional. SKU of the AI Foundry / Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." + } + }, + "allowProjectManagement": { + "type": "bool", + "metadata": { + "description": "Required. Whether to allow project management in AI Foundry. This is required to enable the AI Foundry UI and project management features." + } + }, + "privateEndpointSubnetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource Id of an existing subnet to use for private connectivity. This is required along with 'privateDnsZoneResourceIds' to establish private endpoints." + } + }, + "agentSubnetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource Id of an existing subnet to use for agent connectivity. This is required when using agents with private endpoints." + } + }, + "disableLocalAuth": { + "type": "bool", + "metadata": { + "description": "Required. Allow only Azure AD authentication. Should be enabled for security reasons." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the role assignments for the AI Foundry resource." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of AI Foundry resources." + } + }, + "aiModelDeployments": { + "type": "array", + "items": { + "$ref": "#/definitions/deploymentType" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. Specifies the OpenAI deployments to create." + } + }, + "privateDnsZoneResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of private DNS zone resource IDs to use for the AI Foundry resource. This is required when using private endpoints." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Resources/resourceGroups@2025-04-01#properties/tags" + }, + "description": "Optional. Specifies the resource tags for all the resources." + }, + "defaultValue": {} + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneResourceIdValues", + "count": "[length(coalesce(parameters('privateDnsZoneResourceIds'), createArray()))]", + "input": { + "privateDnsZoneResourceId": "[coalesce(parameters('privateDnsZoneResourceIds'), createArray())[copyIndex('privateDnsZoneResourceIdValues')]]" + } + } + ], + "privateNetworkingEnabled": "[and(not(empty(variables('privateDnsZoneResourceIdValues'))), not(empty(parameters('privateEndpointSubnetResourceId'))))]" + }, + "resources": { + "foundryAccount": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.cognitive-services.account.{0}', parameters('name')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "sku": { + "value": "[parameters('sku')]" + }, + "kind": { + "value": "AIServices" + }, + "lock": { + "value": "[parameters('lock')]" + }, + "allowProjectManagement": { + "value": "[parameters('allowProjectManagement')]" + }, + "managedIdentities": { + "value": { + "systemAssigned": true + } + }, + "deployments": { + "value": "[parameters('aiModelDeployments')]" + }, + "customSubDomainName": { + "value": "[parameters('name')]" + }, + "disableLocalAuth": { + "value": "[parameters('disableLocalAuth')]" + }, + "publicNetworkAccess": "[if(variables('privateNetworkingEnabled'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "networkAcls": { + "value": { + "defaultAction": "Allow", + "bypass": "AzureServices" + } + }, + "networkInjections": "[if(and(variables('privateNetworkingEnabled'), not(empty(parameters('agentSubnetResourceId')))), createObject('value', createObject('scenario', 'agent', 'subnetResourceId', parameters('agentSubnetResourceId'), 'useMicrosoftManagedNetwork', false())), createObject('value', null()))]", + "privateEndpoints": "[if(variables('privateNetworkingEnabled'), createObject('value', createArray(createObject('privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', variables('privateDnsZoneResourceIdValues')), 'subnetResourceId', parameters('privateEndpointSubnetResourceId')))), createObject('value', createArray()))]", + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "roleAssignments": { + "value": "[parameters('roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9381727816193702843" + }, + "name": "Cognitive Services", + "description": "This module deploys a Cognitive Service." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the private endpoint output." + } + }, + "deploymentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of cognitive service account deployment." + } + }, + "model": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of Cognitive Services account deployment model." + } + }, + "format": { + "type": "string", + "metadata": { + "description": "Required. The format of Cognitive Services account deployment model." + } + }, + "version": { + "type": "string", + "metadata": { + "description": "Required. The version of Cognitive Services account deployment model." + } + } + }, + "metadata": { + "description": "Required. Properties of Cognitive Services account deployment model." + } + }, + "sku": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource model definition representing SKU." + } + }, + "capacity": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The capacity of the resource model definition representing SKU." + } + }, + "tier": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The tier of the resource model definition representing SKU." + } + }, + "size": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The size of the resource model definition representing SKU." + } + }, + "family": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The family of the resource model definition representing SKU." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource model definition representing SKU." + } + }, + "raiPolicyName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of RAI policy." + } + }, + "versionUpgradeOption": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version upgrade option." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cognitive services account deployment." + } + }, + "endpointType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Type of the endpoint." + } + }, + "endpoint": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The endpoint URI." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cognitive services account endpoint." + } + }, + "secretsExportConfigurationType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The key vault name where to store the keys and connection strings generated by the modules." + } + }, + "accessKey1Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name for the accessKey1 secret to create." + } + }, + "accessKey2Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name for the accessKey2 secret to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of the secrets exported to the provided Key Vault." + } + }, + "commitmentPlanType": { + "type": "object", + "properties": { + "autoRenew": { + "type": "bool", + "metadata": { + "description": "Required. Whether the plan should auto-renew at the end of the current commitment period." + } + }, + "current": { + "type": "object", + "properties": { + "count": { + "type": "int", + "metadata": { + "description": "Required. The number of committed instances (e.g., number of containers or cores)." + } + }, + "tier": { + "type": "string", + "metadata": { + "description": "Required. The tier of the commitment plan (e.g., T1, T2)." + } + } + }, + "metadata": { + "description": "Required. The current commitment configuration." + } + }, + "hostingModel": { + "type": "string", + "metadata": { + "description": "Required. The hosting model for the commitment plan. (e.g., DisconnectedContainer, ConnectedContainer, ProvisionedWeb, Web)." + } + }, + "planType": { + "type": "string", + "metadata": { + "description": "Required. The plan type indicating which capability the plan applies to (e.g., NTTS, STT, CUSTOMSTT, ADDON)." + } + }, + "commitmentPlanGuid": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique identifier of an existing commitment plan to update. Set to null to create a new plan." + } + }, + "next": { + "type": "object", + "properties": { + "count": { + "type": "int", + "metadata": { + "description": "Required. The number of committed instances for the next period." + } + }, + "tier": { + "type": "string", + "metadata": { + "description": "Required. The tier for the next commitment period." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The configuration of the next commitment period, if scheduled." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a disconnected container commitment plan." + } + }, + "networkInjectionType": { + "type": "object", + "properties": { + "scenario": { + "type": "string", + "allowedValues": [ + "agent", + "none" + ], + "metadata": { + "description": "Required. The scenario for the network injection." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. The Resource ID of the subnet on the Virtual Network on which to inject." + } + }, + "useMicrosoftManagedNetwork": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Whether to use Microsoft Managed Network. Defaults to false." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Type for network configuration in AI Foundry where virtual network injection occurs to secure scenarios like Agents entirely within a private network." + } + }, + "_1.secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "_2.lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_2.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_2.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_2.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_2.roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "customerManagedKeyType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + } + }, + "keyName": { + "type": "string", + "metadata": { + "description": "Required. The name of the customer managed key to use for encryption." + } + }, + "keyVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, the deployment will use the latest version available at deployment time." + } + }, + "userAssignedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type does not support auto-rotation of the customer-managed key.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "privateEndpointSingleServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private Endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the Private Endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_2.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_2.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_2.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + } + }, + "lock": { + "$ref": "#/definitions/_2.lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_2.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "secretsOutputType": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/definitions/_1.secretSetOutputType", + "metadata": { + "description": "An exported secret's references." + } + }, + "metadata": { + "description": "A map of the exported secrets", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of Cognitive Services account." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "AIServices", + "AnomalyDetector", + "CognitiveServices", + "ComputerVision", + "ContentModerator", + "ContentSafety", + "ConversationalLanguageUnderstanding", + "CustomVision.Prediction", + "CustomVision.Training", + "Face", + "FormRecognizer", + "HealthInsights", + "ImmersiveReader", + "Internal.AllInOne", + "LUIS", + "LUIS.Authoring", + "LanguageAuthoring", + "MetricsAdvisor", + "OpenAI", + "Personalizer", + "QnAMaker.v2", + "SpeechServices", + "TextAnalytics", + "TextTranslation" + ], + "metadata": { + "description": "Required. Kind of the Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." + } + }, + "sku": { + "type": "string", + "defaultValue": "S0", + "allowedValues": [ + "C2", + "C3", + "C4", + "F0", + "F1", + "S", + "S0", + "S1", + "S10", + "S2", + "S3", + "S4", + "S5", + "S6", + "S7", + "S8", + "S9", + "DC0" + ], + "metadata": { + "description": "Optional. SKU of the Cognitive Services account. Use 'Get-AzCognitiveServicesAccountSku' to determine a valid combinations of 'kind' and 'SKU' for your Azure region." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "publicNetworkAccess": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." + } + }, + "customSubDomainName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. Subdomain name used for token-based authentication. Required if 'networkAcls' or 'privateEndpoints' are set." + } + }, + "networkAcls": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. A collection of rules governing the accessibility from specific network locations." + } + }, + "networkInjections": { + "$ref": "#/definitions/networkInjectionType", + "nullable": true, + "metadata": { + "description": "Optional. Specifies in AI Foundry where virtual network injection occurs to secure scenarios like Agents entirely within a private network." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + }, + "allowedFqdnList": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. List of allowed FQDN." + } + }, + "apiProperties": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The API properties for special APIs." + } + }, + "disableLocalAuth": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Allow only Azure AD authentication. Should be enabled for security reasons." + } + }, + "customerManagedKey": { + "$ref": "#/definitions/customerManagedKeyType", + "nullable": true, + "metadata": { + "description": "Optional. The customer managed key definition." + } + }, + "dynamicThrottlingEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The flag to enable dynamic throttling." + } + }, + "migrationToken": { + "type": "securestring", + "nullable": true, + "metadata": { + "description": "Optional. Resource migration token." + } + }, + "restore": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Restore a soft-deleted cognitive service at deployment time. Will fail if no such soft-deleted resource exists." + } + }, + "restrictOutboundNetworkAccess": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Restrict outbound network access." + } + }, + "userOwnedStorage": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.CognitiveServices/accounts@2025-04-01-preview#properties/properties/properties/userOwnedStorage" + }, + "description": "Optional. The storage accounts for this resource." + }, + "nullable": true + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "deployments": { + "type": "array", + "items": { + "$ref": "#/definitions/deploymentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of deployments about cognitive service accounts to create." + } + }, + "secretsExportConfiguration": { + "$ref": "#/definitions/secretsExportConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Key vault reference and secret settings for the module's secrets export." + } + }, + "allowProjectManagement": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable project management feature for AI Foundry." + } + }, + "commitmentPlans": { + "type": "array", + "items": { + "$ref": "#/definitions/commitmentPlanType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Commitment plans to deploy for the cognitive services account." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned, UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Cognitive Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '25fbc0a9-bd7c-42a3-aa1a-3b75d497ee68')]", + "Cognitive Services Custom Vision Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c1ff6cc2-c111-46fe-8896-e0ef812ad9f3')]", + "Cognitive Services Custom Vision Deployment": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5c4089e1-6d96-4d2f-b296-c1bc7137275f')]", + "Cognitive Services Custom Vision Labeler": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '88424f51-ebe7-446f-bc41-7fa16989e96c')]", + "Cognitive Services Custom Vision Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '93586559-c37d-4a6b-ba08-b9f0940c2d73')]", + "Cognitive Services Custom Vision Trainer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a5ae4ab-0d65-4eeb-be61-29fc9b54394b')]", + "Cognitive Services Data Reader (Preview)": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b59867f0-fa02-499b-be73-45a86b5b3e1c')]", + "Cognitive Services Face Recognizer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '9894cab4-e18a-44aa-828b-cb588cd6f2d7')]", + "Cognitive Services Immersive Reader User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b2de6794-95db-4659-8781-7e080d3f2b9d')]", + "Cognitive Services Language Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f07febfe-79bc-46b1-8b37-790e26e6e498')]", + "Cognitive Services Language Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7628b7b8-a8b2-4cdc-b46f-e9b35248918e')]", + "Cognitive Services Language Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f2310ca1-dc64-4889-bb49-c8e0fa3d47a8')]", + "Cognitive Services LUIS Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f72c8140-2111-481c-87ff-72b910f6e3f8')]", + "Cognitive Services LUIS Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18e81cdc-4e98-4e29-a639-e7d10c5a6226')]", + "Cognitive Services LUIS Writer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '6322a993-d5c9-4bed-b113-e49bbea25b27')]", + "Cognitive Services Metrics Advisor Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'cb43c632-a144-4ec5-977c-e80c4affc34a')]", + "Cognitive Services Metrics Advisor User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '3b20f47b-3825-43cb-8114-4bd2201156a8')]", + "Cognitive Services OpenAI Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a001fd3d-188f-4b5d-821b-7da978bf7442')]", + "Cognitive Services OpenAI User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')]", + "Cognitive Services QnA Maker Editor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f4cc2bf9-21be-47a1-bdf1-5c5804381025')]", + "Cognitive Services QnA Maker Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '466ccd10-b268-4a11-b098-b4849f024126')]", + "Cognitive Services Speech Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0e75ca1e-0464-4b4d-8b93-68208a576181')]", + "Cognitive Services Speech User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f2dc8367-1007-4938-bd23-fe263f013447')]", + "Cognitive Services User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a97b65f3-24c7-4388-baec-2e87135dc908')]", + "Azure AI Developer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '64702f94-c441-49e6-a78b-ef80e0188fee')]", + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "cMKKeyVault::cMKKey": { + "condition": "[and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), not(empty(tryGet(parameters('customerManagedKey'), 'keyName')))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.cognitiveservices-account.{0}.{1}', replace('0.13.2', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "cMKKeyVault": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId')))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + }, + "cMKUserAssignedIdentity": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2025-01-31-preview", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + }, + "cognitiveService": { + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[parameters('name')]", + "kind": "[parameters('kind')]", + "identity": "[variables('identity')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "allowProjectManagement": "[parameters('allowProjectManagement')]", + "customSubDomainName": "[parameters('customSubDomainName')]", + "networkAcls": "[if(not(empty(coalesce(parameters('networkAcls'), createObject()))), createObject('defaultAction', tryGet(parameters('networkAcls'), 'defaultAction'), 'virtualNetworkRules', coalesce(tryGet(parameters('networkAcls'), 'virtualNetworkRules'), createArray()), 'ipRules', coalesce(tryGet(parameters('networkAcls'), 'ipRules'), createArray())), null())]", + "networkInjections": "[if(not(empty(parameters('networkInjections'))), createArray(createObject('scenario', tryGet(parameters('networkInjections'), 'scenario'), 'subnetArmId', tryGet(parameters('networkInjections'), 'subnetResourceId'), 'useMicrosoftManagedNetwork', coalesce(tryGet(parameters('networkInjections'), 'useMicrosoftManagedNetwork'), false()))), null())]", + "publicNetworkAccess": "[if(not(equals(parameters('publicNetworkAccess'), null())), parameters('publicNetworkAccess'), if(not(empty(parameters('networkAcls'))), 'Enabled', 'Disabled'))]", + "allowedFqdnList": "[parameters('allowedFqdnList')]", + "apiProperties": "[parameters('apiProperties')]", + "disableLocalAuth": "[parameters('disableLocalAuth')]", + "encryption": "[if(not(empty(parameters('customerManagedKey'))), createObject('keySource', 'Microsoft.KeyVault', 'keyVaultProperties', createObject('identityClientId', if(not(empty(coalesce(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), ''))), reference('cMKUserAssignedIdentity').clientId, null()), 'keyVaultUri', reference('cMKKeyVault').vaultUri, 'keyName', parameters('customerManagedKey').keyName, 'keyVersion', if(not(empty(coalesce(tryGet(parameters('customerManagedKey'), 'keyVersion'), ''))), tryGet(parameters('customerManagedKey'), 'keyVersion'), last(split(reference('cMKKeyVault::cMKKey').keyUriWithVersion, '/'))))), null())]", + "migrationToken": "[parameters('migrationToken')]", + "restore": "[parameters('restore')]", + "restrictOutboundNetworkAccess": "[parameters('restrictOutboundNetworkAccess')]", + "userOwnedStorage": "[if(not(empty(parameters('userOwnedStorage'))), parameters('userOwnedStorage'), null())]", + "dynamicThrottlingEnabled": "[parameters('dynamicThrottlingEnabled')]" + }, + "dependsOn": [ + "cMKKeyVault", + "cMKKeyVault::cMKKey", + "cMKUserAssignedIdentity" + ] + }, + "cognitiveService_deployments": { + "copy": { + "name": "cognitiveService_deployments", + "count": "[length(coalesce(parameters('deployments'), createArray()))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.CognitiveServices/accounts/deployments", + "apiVersion": "2025-06-01", + "name": "[format('{0}/{1}', parameters('name'), coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'name'), format('{0}-deployments', parameters('name'))))]", + "properties": { + "model": "[coalesce(parameters('deployments'), createArray())[copyIndex()].model]", + "raiPolicyName": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'raiPolicyName')]", + "versionUpgradeOption": "[tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'versionUpgradeOption')]" + }, + "sku": "[coalesce(tryGet(coalesce(parameters('deployments'), createArray())[copyIndex()], 'sku'), createObject('name', parameters('sku'), 'capacity', tryGet(parameters('sku'), 'capacity'), 'tier', tryGet(parameters('sku'), 'tier'), 'size', tryGet(parameters('sku'), 'size'), 'family', tryGet(parameters('sku'), 'family')))]", + "dependsOn": [ + "cognitiveService" + ] + }, + "cognitiveService_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "cognitiveService" + ] + }, + "cognitiveService_commitmentPlans": { + "copy": { + "name": "cognitiveService_commitmentPlans", + "count": "[length(coalesce(parameters('commitmentPlans'), createArray()))]" + }, + "type": "Microsoft.CognitiveServices/accounts/commitmentPlans", + "apiVersion": "2025-06-01", + "name": "[format('{0}/{1}', parameters('name'), format('{0}-{1}', coalesce(parameters('commitmentPlans'), createArray())[copyIndex()].hostingModel, coalesce(parameters('commitmentPlans'), createArray())[copyIndex()].planType))]", + "properties": "[coalesce(parameters('commitmentPlans'), createArray())[copyIndex()]]", + "dependsOn": [ + "cognitiveService" + ] + }, + "cognitiveService_diagnosticSettings": { + "copy": { + "name": "cognitiveService_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "cognitiveService" + ] + }, + "cognitiveService_roleAssignments": { + "copy": { + "name": "cognitiveService_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "cognitiveService" + ] + }, + "cognitiveService_privateEndpoints": { + "copy": { + "name": "cognitiveService_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-cognitiveService-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.CognitiveServices/accounts', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'account')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "cognitiveService" + ] + }, + "secretsExport": { + "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", + "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" + }, + "secretsToSet": { + "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'accessKey1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey1Name'), 'value', listKeys('cognitiveService', '2025-06-01').key1)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'accessKey2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey2Name'), 'value', listKeys('cognitiveService', '2025-06-01').key2)), createArray()))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10828079590669389085" + } + }, + "definitions": { + "secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "secretToSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret to set." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret to set." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the secret to set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Key Vault to set the ecrets in." + } + }, + "secretsToSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretToSetType" + }, + "metadata": { + "description": "Required. The secrets to set in the Key Vault." + } + } + }, + "resources": { + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "secrets": { + "copy": { + "name": "secrets", + "count": "[length(parameters('secretsToSet'))]" + }, + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "properties": { + "value": "[parameters('secretsToSet')[copyIndex()].value]" + } + } + }, + "outputs": { + "secretsSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretSetOutputType" + }, + "metadata": { + "description": "The references to the secrets exported to the provided Key Vault." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "input": { + "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", + "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]", + "secretUriWithVersion": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUriWithVersion]" + } + } + } + } + } + }, + "dependsOn": [ + "cognitiveService" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the cognitive services account." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the cognitive services account." + }, + "value": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the cognitive services account was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "endpoint": { + "type": "string", + "metadata": { + "description": "The service endpoint of the cognitive services account." + }, + "value": "[reference('cognitiveService').endpoint]" + }, + "endpoints": { + "$ref": "#/definitions/endpointType", + "metadata": { + "description": "All endpoints available for the cognitive services account, types depends on the cognitive service kind." + }, + "value": "[reference('cognitiveService').endpoints]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('cognitiveService', '2025-06-01', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('cognitiveService', '2025-06-01', 'full').location]" + }, + "exportedSecrets": { + "$ref": "#/definitions/secretsOutputType", + "metadata": { + "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." + }, + "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the congitive services account." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('cognitiveService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the AI Foundry resource." + }, + "value": "[reference('foundryAccount').outputs.name.value]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Foundry resource." + }, + "value": "[reference('foundryAccount').outputs.resourceId.value]" + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID of the AI Foundry resource." + }, + "value": "[subscription().subscriptionId]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource Group Name of the AI Foundry resource." + }, + "value": "[resourceGroup().name]" + }, + "location": { + "type": "string", + "metadata": { + "description": "Location of the AI Foundry resource." + }, + "value": "[parameters('location')]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "metadata": { + "description": "System assigned managed identity principal ID of the AI Foundry resource." + }, + "value": "[reference('foundryAccount').outputs.systemAssignedMIPrincipalId.value]" + } + } + } + } + }, + "keyVault": { + "condition": "[parameters('includeAssociatedResources')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.keyVault.{0}', variables('resourcesName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingResourceId": { + "value": "[tryGet(parameters('keyVaultConfiguration'), 'existingResourceId')]" + }, + "name": { + "value": "[take(if(and(not(empty(parameters('keyVaultConfiguration'))), not(empty(tryGet(parameters('keyVaultConfiguration'), 'name')))), parameters('keyVaultConfiguration').name, format('kv{0}', variables('resourcesName'))), 24)]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "privateEndpointSubnetResourceId": { + "value": "[parameters('privateEndpointSubnetResourceId')]" + }, + "privateDnsZoneResourceId": { + "value": "[tryGet(parameters('keyVaultConfiguration'), 'privateDnsZoneResourceId')]" + }, + "roleAssignments": { + "value": "[tryGet(parameters('keyVaultConfiguration'), 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "9899473530932390252" + } + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "functions": [ + { + "namespace": "__bicep", + "members": { + "getResourceGroupName": { + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(greater(length(parameters('parts')), 4), parameters('parts')[4], resourceGroup().name)]" + }, + "metadata": { + "description": "Extracts the Resource Group Name from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getResourceName": { + "parameters": [ + { + "type": "string", + "nullable": true, + "name": "resourceId" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(and(and(not(empty(parameters('resourceId'))), contains(parameters('resourceId'), '/')), not(empty(parameters('parts')))), last(parameters('parts')), coalesce(parameters('resourceId'), ''))]" + }, + "metadata": { + "description": "Extracts the Resource Name from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getResourceParts": { + "parameters": [ + { + "type": "string", + "nullable": true, + "name": "resourceId" + } + ], + "output": { + "type": "array", + "items": { + "type": "string" + }, + "value": "[split(coalesce(parameters('resourceId'), ''), '/')]" + }, + "metadata": { + "description": "Splits Resource ID into its components.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getSubscriptionId": { + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(greater(length(parameters('parts')), 2), parameters('parts')[2], subscription().subscriptionId)]" + }, + "metadata": { + "description": "Extracts the Subscription ID from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + } + } + } + ], + "parameters": { + "name": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Required. The name of the Key Vault." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Required. The location for the Key Vault." + } + }, + "existingResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full resource ID of an existing Key Vault to use instead of creating a new one." + } + }, + "privateEndpointSubnetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource Id of an existing subnet to use for private connectivity. This is required along with 'privateDnsZoneResourceId' to establish private endpoints." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the private DNS zone for the Key Vault to establish private endpoints." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the role assignments for the Key Vault." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Resources/resourceGroups@2025-04-01#properties/tags" + }, + "description": "Optional. Specifies the resource tags for all the resources." + }, + "defaultValue": {} + } + }, + "variables": { + "existingResourceParts": "[__bicep.getResourceParts(parameters('existingResourceId'))]", + "existingName": "[__bicep.getResourceName(parameters('existingResourceId'), variables('existingResourceParts'))]", + "existingSubscriptionId": "[__bicep.getSubscriptionId(variables('existingResourceParts'))]", + "existingResourceGroupName": "[__bicep.getResourceGroupName(variables('existingResourceParts'))]", + "privateNetworkingEnabled": "[and(not(empty(parameters('privateDnsZoneResourceId'))), not(empty(parameters('privateEndpointSubnetResourceId'))))]" + }, + "resources": { + "existingKeyVault": { + "condition": "[not(empty(parameters('existingResourceId')))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "subscriptionId": "[variables('existingSubscriptionId')]", + "resourceGroup": "[variables('existingResourceGroupName')]", + "name": "[variables('existingName')]" + }, + "keyVault": { + "condition": "[empty(parameters('existingResourceId'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.key-vault.vault.{0}', parameters('name')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "publicNetworkAccess": "[if(variables('privateNetworkingEnabled'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "networkAcls": { + "value": { + "defaultAction": "[if(variables('privateNetworkingEnabled'), 'Deny', 'Allow')]" + } + }, + "enableVaultForDeployment": { + "value": true + }, + "enableVaultForDiskEncryption": { + "value": true + }, + "enableVaultForTemplateDeployment": { + "value": true + }, + "enablePurgeProtection": { + "value": false + }, + "enableRbacAuthorization": { + "value": true + }, + "enableSoftDelete": { + "value": true + }, + "softDeleteRetentionInDays": { + "value": 7 + }, + "privateEndpoints": "[if(variables('privateNetworkingEnabled'), createObject('value', createArray(createObject('privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', parameters('privateDnsZoneResourceId')))), 'service', 'vault', 'subnetResourceId', parameters('privateEndpointSubnetResourceId')))), createObject('value', createArray()))]", + "roleAssignments": { + "value": "[parameters('roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "8811577289487069918" + }, + "name": "Key Vaults", + "description": "This module deploys a Key Vault." + }, + "definitions": { + "networkAclsType": { + "type": "object", + "properties": { + "bypass": { + "type": "string", + "allowedValues": [ + "AzureServices", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. The bypass options for traffic for the network ACLs." + } + }, + "defaultAction": { + "type": "string", + "allowedValues": [ + "Allow", + "Deny" + ], + "nullable": true, + "metadata": { + "description": "Optional. The default action for the network ACLs, when no rule matches." + } + }, + "ipRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string", + "metadata": { + "description": "Required. An IPv4 address range in CIDR notation, such as \"124.56.78.91\" (simple IP address) or \"124.56.78.0/24\"." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP rules." + } + }, + "virtualNetworkRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the virtual network subnet." + } + }, + "ignoreMissingVnetServiceEndpoint": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Whether NRP will ignore the check if parent subnet has serviceEndpoints configured." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of virtual network rules." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for rules governing the accessibility of the key vault from specific network locations." + } + }, + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "credentialOutputType": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The item's resourceId." + } + }, + "uri": { + "type": "string", + "metadata": { + "description": "The item's uri." + } + }, + "uriWithVersion": { + "type": "string", + "metadata": { + "description": "The item's uri with version." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a credential output." + } + }, + "accessPolicyType": { + "type": "object", + "properties": { + "tenantId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The tenant ID that is used for authenticating requests to the key vault." + } + }, + "objectId": { + "type": "string", + "metadata": { + "description": "Required. The object ID of a user, service principal or security group in the tenant for the vault." + } + }, + "applicationId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Application ID of the client making request on behalf of a principal." + } + }, + "permissions": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "allowedValues": [ + "all", + "backup", + "create", + "decrypt", + "delete", + "encrypt", + "get", + "getrotationpolicy", + "import", + "list", + "purge", + "recover", + "release", + "restore", + "rotate", + "setrotationpolicy", + "sign", + "unwrapKey", + "update", + "verify", + "wrapKey" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions to keys." + } + }, + "secrets": { + "type": "array", + "allowedValues": [ + "all", + "backup", + "delete", + "get", + "list", + "purge", + "recover", + "restore", + "set" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions to secrets." + } + }, + "certificates": { + "type": "array", + "allowedValues": [ + "all", + "backup", + "create", + "delete", + "deleteissuers", + "get", + "getissuers", + "import", + "list", + "listissuers", + "managecontacts", + "manageissuers", + "purge", + "recover", + "restore", + "setissuers", + "update" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions to certificates." + } + }, + "storage": { + "type": "array", + "allowedValues": [ + "all", + "backup", + "delete", + "deletesas", + "get", + "getsas", + "list", + "listsas", + "purge", + "recover", + "regeneratekey", + "restore", + "set", + "setsas", + "update" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions to storage accounts." + } + } + }, + "metadata": { + "description": "Required. Permissions the identity has for keys, secrets and certificates." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an access policy." + } + }, + "secretType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Resource tags." + } + }, + "attributes": { + "type": "object", + "properties": { + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Defines whether the secret is enabled or disabled." + } + }, + "exp": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Defines when the secret will become invalid. Defined in seconds since 1970-01-01T00:00:00Z." + } + }, + "nbf": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. If set, defines the date from which onwards the secret becomes valid. Defined in seconds since 1970-01-01T00:00:00Z." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Contains attributes of the secret." + } + }, + "contentType": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The content type of the secret." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret. NOTE: \"value\" will never be returned from the service, as APIs using this model are is intended for internal use in ARM deployments. Users should use the data-plane REST service for interaction with vault secrets." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a secret output." + } + }, + "keyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the key." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Resource tags." + } + }, + "attributes": { + "type": "object", + "properties": { + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Defines whether the key is enabled or disabled." + } + }, + "exp": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Defines when the key will become invalid. Defined in seconds since 1970-01-01T00:00:00Z." + } + }, + "nbf": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. If set, defines the date from which onwards the key becomes valid. Defined in seconds since 1970-01-01T00:00:00Z." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Contains attributes of the key." + } + }, + "curveName": { + "type": "string", + "allowedValues": [ + "P-256", + "P-256K", + "P-384", + "P-521" + ], + "nullable": true, + "metadata": { + "description": "Optional. The elliptic curve name. Only works if \"keySize\" equals \"EC\" or \"EC-HSM\". Default is \"P-256\"." + } + }, + "keyOps": { + "type": "array", + "allowedValues": [ + "decrypt", + "encrypt", + "import", + "release", + "sign", + "unwrapKey", + "verify", + "wrapKey" + ], + "nullable": true, + "metadata": { + "description": "Optional. The allowed operations on this key." + } + }, + "keySize": { + "type": "int", + "allowedValues": [ + 2048, + 3072, + 4096 + ], + "nullable": true, + "metadata": { + "description": "Optional. The key size in bits. Only works if \"keySize\" equals \"RSA\" or \"RSA-HSM\". Default is \"4096\"." + } + }, + "kty": { + "type": "string", + "allowedValues": [ + "EC", + "EC-HSM", + "RSA", + "RSA-HSM" + ], + "nullable": true, + "metadata": { + "description": "Optional. The type of the key. Default is \"EC\"." + } + }, + "releasePolicy": { + "type": "object", + "properties": { + "contentType": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Content type and version of key release policy." + } + }, + "data": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Blob encoding the policy rules under which the key can be released." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Key release policy." + } + }, + "rotationPolicy": { + "$ref": "#/definitions/rotationPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Key rotation policy." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a key." + } + }, + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "privateEndpointSingleServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private Endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the Private Endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "rotationPolicyType": { + "type": "object", + "properties": { + "attributes": { + "type": "object", + "properties": { + "expiryTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The expiration time for the new key version. It should be in ISO8601 format. Eg: \"P90D\", \"P1Y\"." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The attributes of key rotation policy." + } + }, + "lifetimeActions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "object", + "properties": { + "type": { + "type": "string", + "allowedValues": [ + "notify", + "rotate" + ], + "nullable": true, + "metadata": { + "description": "Optional. The type of the action." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The type of the action." + } + }, + "trigger": { + "type": "object", + "properties": { + "timeAfterCreate": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The time duration after key creation to rotate the key. It only applies to rotate. It will be in ISO 8601 duration format. Eg: \"P90D\", \"P1Y\"." + } + }, + "timeBeforeExpiry": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The time duration before key expiring to rotate or notify. It will be in ISO 8601 duration format. Eg: \"P90D\", \"P1Y\"." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The time duration for rotating the key." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The key rotation policy lifetime actions." + } + } + }, + "metadata": { + "description": "The type for a rotation policy.", + "__bicep_imported_from!": { + "sourceTemplate": "key/main.bicep" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Required. Name of the Key Vault. Must be globally unique." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "accessPolicies": { + "type": "array", + "items": { + "$ref": "#/definitions/accessPolicyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. All access policies to create." + } + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/definitions/secretType" + }, + "nullable": true, + "metadata": { + "description": "Optional. All secrets to create." + } + }, + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/keyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. All keys to create." + } + }, + "enableVaultForDeployment": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Specifies if the vault is enabled for deployment by script or compute." + } + }, + "enableVaultForTemplateDeployment": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Specifies if the vault is enabled for a template deployment." + } + }, + "enableVaultForDiskEncryption": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Specifies if the azure platform has access to the vault for enabling disk encryption scenarios." + } + }, + "enableSoftDelete": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Switch to enable/disable Key Vault's soft delete feature." + } + }, + "softDeleteRetentionInDays": { + "type": "int", + "defaultValue": 90, + "metadata": { + "description": "Optional. softDelete data retention days. It accepts >=7 and <=90." + } + }, + "enableRbacAuthorization": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Property that controls how data actions are authorized. When true, the key vault will use Role Based Access Control (RBAC) for authorization of data actions, and the access policies specified in vault properties will be ignored. When false, the key vault will use the access policies specified in vault properties, and any policy stored on Azure Resource Manager will be ignored. Note that management actions are always authorized with RBAC." + } + }, + "createMode": { + "type": "string", + "defaultValue": "default", + "allowedValues": [ + "default", + "recover" + ], + "metadata": { + "description": "Optional. The vault's create mode to indicate whether the vault need to be recovered or not." + } + }, + "enablePurgeProtection": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Provide 'true' to enable Key Vault's purge protection feature." + } + }, + "sku": { + "type": "string", + "defaultValue": "premium", + "allowedValues": [ + "premium", + "standard" + ], + "metadata": { + "description": "Optional. Specifies the SKU for the vault." + } + }, + "networkAcls": { + "$ref": "#/definitions/networkAclsType", + "nullable": true, + "metadata": { + "description": "Optional. Rules governing the accessibility of the resource from specific network locations." + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "", + "allowedValues": [ + "", + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.KeyVault/vaults@2024-11-01#properties/tags" + }, + "description": "Optional. Resource tags." + }, + "nullable": true + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + }, + { + "name": "formattedAccessPolicies", + "count": "[length(coalesce(parameters('accessPolicies'), createArray()))]", + "input": { + "applicationId": "[coalesce(tryGet(coalesce(parameters('accessPolicies'), createArray())[copyIndex('formattedAccessPolicies')], 'applicationId'), '')]", + "objectId": "[coalesce(parameters('accessPolicies'), createArray())[copyIndex('formattedAccessPolicies')].objectId]", + "permissions": "[coalesce(parameters('accessPolicies'), createArray())[copyIndex('formattedAccessPolicies')].permissions]", + "tenantId": "[coalesce(tryGet(coalesce(parameters('accessPolicies'), createArray())[copyIndex('formattedAccessPolicies')], 'tenantId'), tenant().tenantId)]" + } + } + ], + "enableReferencedModulesTelemetry": false, + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Key Vault Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')]", + "Key Vault Certificates Officer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a4417e6f-fecd-4de8-b567-7b0420556985')]", + "Key Vault Certificate User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db79e9a7-68ee-4b58-9aeb-b90e7c24fcba')]", + "Key Vault Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f25e0fa2-a7c8-4377-a976-54943a77a395')]", + "Key Vault Crypto Officer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '14b46e9e-c2b7-41b4-b07b-48a6ebf60603')]", + "Key Vault Crypto Service Encryption User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e147488a-f6f5-4113-8e2d-b22465e65bf6')]", + "Key Vault Crypto User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '12338af0-0e69-4776-bea7-57ae8d297424')]", + "Key Vault Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '21090545-7ca7-4776-b22c-e363652d74d2')]", + "Key Vault Secrets Officer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7')]", + "Key Vault Secrets User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.keyvault-vault.{0}.{1}', replace('0.13.3', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "keyVault": { + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "enabledForDeployment": "[parameters('enableVaultForDeployment')]", + "enabledForTemplateDeployment": "[parameters('enableVaultForTemplateDeployment')]", + "enabledForDiskEncryption": "[parameters('enableVaultForDiskEncryption')]", + "enableSoftDelete": "[parameters('enableSoftDelete')]", + "softDeleteRetentionInDays": "[parameters('softDeleteRetentionInDays')]", + "enableRbacAuthorization": "[parameters('enableRbacAuthorization')]", + "createMode": "[parameters('createMode')]", + "enablePurgeProtection": "[if(parameters('enablePurgeProtection'), parameters('enablePurgeProtection'), null())]", + "tenantId": "[subscription().tenantId]", + "accessPolicies": "[variables('formattedAccessPolicies')]", + "sku": { + "name": "[parameters('sku')]", + "family": "A" + }, + "networkAcls": "[if(not(empty(coalesce(parameters('networkAcls'), createObject()))), createObject('bypass', tryGet(parameters('networkAcls'), 'bypass'), 'defaultAction', tryGet(parameters('networkAcls'), 'defaultAction'), 'virtualNetworkRules', coalesce(tryGet(parameters('networkAcls'), 'virtualNetworkRules'), createArray()), 'ipRules', coalesce(tryGet(parameters('networkAcls'), 'ipRules'), createArray())), null())]", + "publicNetworkAccess": "[if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(and(not(empty(coalesce(parameters('privateEndpoints'), createArray()))), empty(coalesce(parameters('networkAcls'), createObject()))), 'Disabled', null()))]" + } + }, + "keyVault_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "keyVault" + ] + }, + "keyVault_diagnosticSettings": { + "copy": { + "name": "keyVault_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "keyVault" + ] + }, + "keyVault_roleAssignments": { + "copy": { + "name": "keyVault_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.KeyVault/vaults', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "keyVault" + ] + }, + "keyVault_accessPolicies": { + "condition": "[not(empty(parameters('accessPolicies')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-KeyVault-AccessPolicies', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[parameters('name')]" + }, + "accessPolicies": { + "value": "[parameters('accessPolicies')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "8803020983329720581" + }, + "name": "Key Vault Access Policies", + "description": "This module deploys a Key Vault Access Policy." + }, + "definitions": { + "accessPoliciesType": { + "type": "object", + "properties": { + "tenantId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The tenant ID that is used for authenticating requests to the key vault." + } + }, + "objectId": { + "type": "string", + "metadata": { + "description": "Required. The object ID of a user, service principal or security group in the tenant for the vault." + } + }, + "applicationId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Application ID of the client making request on behalf of a principal." + } + }, + "permissions": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "allowedValues": [ + "all", + "backup", + "create", + "decrypt", + "delete", + "encrypt", + "get", + "getrotationpolicy", + "import", + "list", + "purge", + "recover", + "release", + "restore", + "rotate", + "setrotationpolicy", + "sign", + "unwrapKey", + "update", + "verify", + "wrapKey" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions to keys." + } + }, + "secrets": { + "type": "array", + "allowedValues": [ + "all", + "backup", + "delete", + "get", + "list", + "purge", + "recover", + "restore", + "set" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions to secrets." + } + }, + "certificates": { + "type": "array", + "allowedValues": [ + "all", + "backup", + "create", + "delete", + "deleteissuers", + "get", + "getissuers", + "import", + "list", + "listissuers", + "managecontacts", + "manageissuers", + "purge", + "recover", + "restore", + "setissuers", + "update" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions to certificates." + } + }, + "storage": { + "type": "array", + "allowedValues": [ + "all", + "backup", + "delete", + "deletesas", + "get", + "getsas", + "list", + "listsas", + "purge", + "recover", + "regeneratekey", + "restore", + "set", + "setsas", + "update" + ], + "nullable": true, + "metadata": { + "description": "Optional. Permissions to storage accounts." + } + } + }, + "metadata": { + "description": "Required. Permissions the identity has for keys, secrets and certificates." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an access policy." + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent key vault. Required if the template is used in a standalone deployment." + } + }, + "accessPolicies": { + "type": "array", + "items": { + "$ref": "#/definitions/accessPoliciesType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of 0 to 16 identities that have access to the key vault. All identities in the array must use the same tenant ID as the key vault's tenant ID." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.keyvault-accesspolicy.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "policies": { + "type": "Microsoft.KeyVault/vaults/accessPolicies", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), 'add')]", + "properties": { + "copy": [ + { + "name": "accessPolicies", + "count": "[length(coalesce(parameters('accessPolicies'), createArray()))]", + "input": { + "applicationId": "[coalesce(tryGet(coalesce(parameters('accessPolicies'), createArray())[copyIndex('accessPolicies')], 'applicationId'), '')]", + "objectId": "[coalesce(parameters('accessPolicies'), createArray())[copyIndex('accessPolicies')].objectId]", + "permissions": "[coalesce(parameters('accessPolicies'), createArray())[copyIndex('accessPolicies')].permissions]", + "tenantId": "[coalesce(tryGet(coalesce(parameters('accessPolicies'), createArray())[copyIndex('accessPolicies')], 'tenantId'), tenant().tenantId)]" + } + } + ] + } + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the access policies assignment was created in." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the access policies assignment." + }, + "value": "add" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the access policies assignment." + }, + "value": "[resourceId('Microsoft.KeyVault/vaults/accessPolicies', parameters('keyVaultName'), 'add')]" + } + } + } + }, + "dependsOn": [ + "keyVault" + ] + }, + "keyVault_secrets": { + "copy": { + "name": "keyVault_secrets", + "count": "[length(coalesce(parameters('secrets'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-KeyVault-Secret-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('secrets'), createArray())[copyIndex()].name]" + }, + "value": { + "value": "[coalesce(parameters('secrets'), createArray())[copyIndex()].value]" + }, + "keyVaultName": { + "value": "[parameters('name')]" + }, + "attributesEnabled": { + "value": "[tryGet(tryGet(coalesce(parameters('secrets'), createArray())[copyIndex()], 'attributes'), 'enabled')]" + }, + "attributesExp": { + "value": "[tryGet(tryGet(coalesce(parameters('secrets'), createArray())[copyIndex()], 'attributes'), 'exp')]" + }, + "attributesNbf": { + "value": "[tryGet(tryGet(coalesce(parameters('secrets'), createArray())[copyIndex()], 'attributes'), 'nbf')]" + }, + "contentType": { + "value": "[tryGet(coalesce(parameters('secrets'), createArray())[copyIndex()], 'contentType')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('secrets'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('secrets'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "8701309639990049090" + }, + "name": "Key Vault Secrets", + "description": "This module deploys a Key Vault Secret." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent key vault. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 127, + "metadata": { + "description": "Required. The name of the secret (letters (upper and lower case), numbers, -)." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.KeyVault/vaults/secrets@2024-11-01#properties/tags" + }, + "description": "Optional. Resource tags." + }, + "nullable": true + }, + "attributesEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Determines whether the object is enabled." + } + }, + "attributesExp": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Expiry date in seconds since 1970-01-01T00:00:00Z. For security reasons, it is recommended to set an expiration date whenever possible." + } + }, + "attributesNbf": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Not before date in seconds since 1970-01-01T00:00:00Z." + } + }, + "contentType": { + "type": "securestring", + "nullable": true, + "metadata": { + "description": "Optional. The content type of the secret." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret. NOTE: \"value\" will never be returned from the service, as APIs using this model are is intended for internal use in ARM deployments. Users should use the data-plane REST service for interaction with vault secrets." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Key Vault Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')]", + "Key Vault Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f25e0fa2-a7c8-4377-a976-54943a77a395')]", + "Key Vault Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '21090545-7ca7-4776-b22c-e363652d74d2')]", + "Key Vault Secrets Officer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b86a8fe4-44ce-4948-aee5-eccb2c155cd7')]", + "Key Vault Secrets User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.keyvault-secret.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "secret": { + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "contentType": "[parameters('contentType')]", + "attributes": { + "enabled": "[parameters('attributesEnabled')]", + "exp": "[parameters('attributesExp')]", + "nbf": "[parameters('attributesNbf')]" + }, + "value": "[parameters('value')]" + } + }, + "secret_roleAssignments": { + "copy": { + "name": "secret_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.KeyVault/vaults/{0}/secrets/{1}', parameters('keyVaultName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "secret" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the secret." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the secret." + }, + "value": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('name'))]" + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The uri of the secret." + }, + "value": "[reference('secret').secretUri]" + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The uri with version of the secret." + }, + "value": "[reference('secret').secretUriWithVersion]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the secret was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "keyVault" + ] + }, + "keyVault_keys": { + "copy": { + "name": "keyVault_keys", + "count": "[length(coalesce(parameters('keys'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-KeyVault-Key-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('keys'), createArray())[copyIndex()].name]" + }, + "keyVaultName": { + "value": "[parameters('name')]" + }, + "attributesEnabled": { + "value": "[tryGet(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'attributes'), 'enabled')]" + }, + "attributesExp": { + "value": "[tryGet(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'attributes'), 'exp')]" + }, + "attributesNbf": { + "value": "[tryGet(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'attributes'), 'nbf')]" + }, + "curveName": "[if(and(not(equals(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'kty'), 'RSA')), not(equals(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'kty'), 'RSA-HSM'))), createObject('value', coalesce(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'curveName'), 'P-256')), createObject('value', null()))]", + "keyOps": { + "value": "[tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'keyOps')]" + }, + "keySize": "[if(or(equals(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'kty'), 'RSA'), equals(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'kty'), 'RSA-HSM')), createObject('value', coalesce(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'keySize'), 4096)), createObject('value', null()))]", + "releasePolicy": { + "value": "[coalesce(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'releasePolicy'), createObject())]" + }, + "kty": { + "value": "[coalesce(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'kty'), 'EC')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "rotationPolicy": { + "value": "[tryGet(coalesce(parameters('keys'), createArray())[copyIndex()], 'rotationPolicy')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "1266219369073699726" + }, + "name": "Key Vault Keys", + "description": "This module deploys a Key Vault Key." + }, + "definitions": { + "rotationPolicyType": { + "type": "object", + "properties": { + "attributes": { + "type": "object", + "properties": { + "expiryTime": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The expiration time for the new key version. It should be in ISO8601 format. Eg: \"P90D\", \"P1Y\"." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The attributes of key rotation policy." + } + }, + "lifetimeActions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action": { + "type": "object", + "properties": { + "type": { + "type": "string", + "allowedValues": [ + "notify", + "rotate" + ], + "nullable": true, + "metadata": { + "description": "Optional. The type of the action." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The type of the action." + } + }, + "trigger": { + "type": "object", + "properties": { + "timeAfterCreate": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The time duration after key creation to rotate the key. It only applies to rotate. It will be in ISO 8601 duration format. Eg: \"P90D\", \"P1Y\"." + } + }, + "timeBeforeExpiry": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The time duration before key expiring to rotate or notify. It will be in ISO 8601 duration format. Eg: \"P90D\", \"P1Y\"." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The time duration for rotating the key." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The key rotation policy lifetime actions." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a rotation policy." + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent key vault. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the key." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.KeyVault/vaults/keys@2024-11-01#properties/tags" + }, + "description": "Optional. Resource tags." + }, + "nullable": true + }, + "attributesEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Determines whether the object is enabled." + } + }, + "attributesExp": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Expiry date in seconds since 1970-01-01T00:00:00Z. For security reasons, it is recommended to set an expiration date whenever possible." + } + }, + "attributesNbf": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Not before date in seconds since 1970-01-01T00:00:00Z." + } + }, + "curveName": { + "type": "string", + "defaultValue": "P-256", + "allowedValues": [ + "P-256", + "P-256K", + "P-384", + "P-521" + ], + "metadata": { + "description": "Optional. The elliptic curve name." + } + }, + "keyOps": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "allowedValues": [ + "decrypt", + "encrypt", + "import", + "sign", + "unwrapKey", + "verify", + "wrapKey" + ], + "metadata": { + "description": "Optional. Array of JsonWebKeyOperation." + } + }, + "keySize": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The key size in bits. For example: 2048, 3072, or 4096 for RSA." + } + }, + "kty": { + "type": "string", + "defaultValue": "EC", + "allowedValues": [ + "EC", + "EC-HSM", + "RSA", + "RSA-HSM" + ], + "metadata": { + "description": "Optional. The type of the key." + } + }, + "releasePolicy": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Key release policy." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "rotationPolicy": { + "$ref": "#/definitions/rotationPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Key rotation policy properties object." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Key Vault Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '00482a5a-887f-4fb3-b363-3b7fe8e74483')]", + "Key Vault Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f25e0fa2-a7c8-4377-a976-54943a77a395')]", + "Key Vault Crypto Officer": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '14b46e9e-c2b7-41b4-b07b-48a6ebf60603')]", + "Key Vault Crypto Service Encryption User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e147488a-f6f5-4113-8e2d-b22465e65bf6')]", + "Key Vault Crypto User": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '12338af0-0e69-4776-bea7-57ae8d297424')]", + "Key Vault Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '21090545-7ca7-4776-b22c-e363652d74d2')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.keyvault-key.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "key": { + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": "[shallowMerge(createArray(createObject('attributes', createObject('enabled', parameters('attributesEnabled'), 'exp', parameters('attributesExp'), 'nbf', parameters('attributesNbf')), 'curveName', parameters('curveName'), 'keyOps', parameters('keyOps'), 'keySize', parameters('keySize'), 'kty', parameters('kty'), 'release_policy', coalesce(parameters('releasePolicy'), createObject())), if(not(empty(parameters('rotationPolicy'))), createObject('rotationPolicy', parameters('rotationPolicy')), createObject())))]" + }, + "key_roleAssignments": { + "copy": { + "name": "key_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.KeyVault/vaults/{0}/keys/{1}', parameters('keyVaultName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.KeyVault/vaults/keys', parameters('keyVaultName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "key" + ] + } + }, + "outputs": { + "keyUri": { + "type": "string", + "metadata": { + "description": "The uri of the key." + }, + "value": "[reference('key').keyUri]" + }, + "keyUriWithVersion": { + "type": "string", + "metadata": { + "description": "The uri with version of the key." + }, + "value": "[reference('key').keyUriWithVersion]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the key." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the key." + }, + "value": "[resourceId('Microsoft.KeyVault/vaults/keys', parameters('keyVaultName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the key was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "keyVault" + ] + }, + "keyVault_privateEndpoints": { + "copy": { + "name": "keyVault_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-keyVault-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.KeyVault/vaults', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'vault'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.KeyVault/vaults', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'vault'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.KeyVault/vaults', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'vault')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.KeyVault/vaults', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'vault'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.KeyVault/vaults', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'vault')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "keyVault" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the key vault." + }, + "value": "[resourceId('Microsoft.KeyVault/vaults', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the key vault was created in." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the key vault." + }, + "value": "[parameters('name')]" + }, + "uri": { + "type": "string", + "metadata": { + "description": "The URI of the key vault." + }, + "value": "[reference('keyVault').vaultUri]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('keyVault', '2024-11-01', 'full').location]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the key vault." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('keyVault_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('keyVault_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('keyVault_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('keyVault_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('keyVault_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/definitions/credentialOutputType" + }, + "metadata": { + "description": "The properties of the created secrets." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secrets'), createArray()))))]", + "input": { + "resourceId": "[reference(format('keyVault_secrets[{0}]', range(0, length(coalesce(parameters('secrets'), createArray())))[copyIndex()])).outputs.resourceId.value]", + "uri": "[reference(format('keyVault_secrets[{0}]', range(0, length(coalesce(parameters('secrets'), createArray())))[copyIndex()])).outputs.secretUri.value]", + "uriWithVersion": "[reference(format('keyVault_secrets[{0}]', range(0, length(coalesce(parameters('secrets'), createArray())))[copyIndex()])).outputs.secretUriWithVersion.value]" + } + } + }, + "keys": { + "type": "array", + "items": { + "$ref": "#/definitions/credentialOutputType" + }, + "metadata": { + "description": "The properties of the created keys." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('keys'), createArray()))))]", + "input": { + "resourceId": "[reference(format('keyVault_keys[{0}]', range(0, length(coalesce(parameters('keys'), createArray())))[copyIndex()])).outputs.resourceId.value]", + "uri": "[reference(format('keyVault_keys[{0}]', range(0, length(coalesce(parameters('keys'), createArray())))[copyIndex()])).outputs.keyUri.value]", + "uriWithVersion": "[reference(format('keyVault_keys[{0}]', range(0, length(coalesce(parameters('keys'), createArray())))[copyIndex()])).outputs.keyUriWithVersion.value]" + } + } + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Key Vault." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('keyVault').outputs.name.value, variables('existingName'))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Key Vault." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('keyVault').outputs.resourceId.value, extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingSubscriptionId'), variables('existingResourceGroupName')), 'Microsoft.KeyVault/vaults', variables('existingName')))]" + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID of the Key Vault." + }, + "value": "[if(empty(parameters('existingResourceId')), subscription().subscriptionId, variables('existingSubscriptionId'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource Group Name of the Key Vault." + }, + "value": "[if(empty(parameters('existingResourceId')), resourceGroup().name, variables('existingResourceGroupName'))]" + } + } + } + } + }, + "aiSearch": { + "condition": "[parameters('includeAssociatedResources')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.aiSearch.{0}', variables('resourcesName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingResourceId": { + "value": "[tryGet(parameters('aiSearchConfiguration'), 'existingResourceId')]" + }, + "name": { + "value": "[take(if(not(empty(tryGet(parameters('aiSearchConfiguration'), 'name'))), parameters('aiSearchConfiguration').name, format('srch{0}', variables('resourcesName'))), 60)]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "privateEndpointSubnetResourceId": { + "value": "[parameters('privateEndpointSubnetResourceId')]" + }, + "privateDnsZoneResourceId": { + "value": "[tryGet(parameters('aiSearchConfiguration'), 'privateDnsZoneResourceId')]" + }, + "roleAssignments": { + "value": "[tryGet(parameters('aiSearchConfiguration'), 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "2804046174313565921" + } + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "functions": [ + { + "namespace": "__bicep", + "members": { + "getResourceGroupName": { + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(greater(length(parameters('parts')), 4), parameters('parts')[4], resourceGroup().name)]" + }, + "metadata": { + "description": "Extracts the Resource Group Name from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getResourceName": { + "parameters": [ + { + "type": "string", + "nullable": true, + "name": "resourceId" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(and(and(not(empty(parameters('resourceId'))), contains(parameters('resourceId'), '/')), not(empty(parameters('parts')))), last(parameters('parts')), coalesce(parameters('resourceId'), ''))]" + }, + "metadata": { + "description": "Extracts the Resource Name from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getResourceParts": { + "parameters": [ + { + "type": "string", + "nullable": true, + "name": "resourceId" + } + ], + "output": { + "type": "array", + "items": { + "type": "string" + }, + "value": "[split(coalesce(parameters('resourceId'), ''), '/')]" + }, + "metadata": { + "description": "Splits Resource ID into its components.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getSubscriptionId": { + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(greater(length(parameters('parts')), 2), parameters('parts')[2], subscription().subscriptionId)]" + }, + "metadata": { + "description": "Extracts the Subscription ID from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + } + } + } + ], + "parameters": { + "name": { + "type": "string", + "maxLength": 60, + "metadata": { + "description": "Required. The name of the AI Search resource." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Required. The location for the AI Search resource." + } + }, + "existingResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full resource ID of an existing AI Search resource to use instead of creating a new one." + } + }, + "privateEndpointSubnetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource Id of an existing subnet to use for private connectivity. This is required along with 'privateDnsZoneResourceId' to establish private endpoints." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the private DNS zone for the AI Search resource to establish private endpoints." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the role assignments for the AI Search resource." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Resources/resourceGroups@2025-04-01#properties/tags" + }, + "description": "Optional. Specifies the resource tags for all the resources." + }, + "defaultValue": {} + } + }, + "variables": { + "existingResourceParts": "[__bicep.getResourceParts(parameters('existingResourceId'))]", + "existingName": "[__bicep.getResourceName(parameters('existingResourceId'), variables('existingResourceParts'))]", + "existingSubscriptionId": "[__bicep.getSubscriptionId(variables('existingResourceParts'))]", + "existingResourceGroupName": "[__bicep.getResourceGroupName(variables('existingResourceParts'))]", + "privateNetworkingEnabled": "[and(not(empty(parameters('privateDnsZoneResourceId'))), not(empty(parameters('privateEndpointSubnetResourceId'))))]" + }, + "resources": { + "existingSearchService": { + "condition": "[not(empty(parameters('existingResourceId')))]", + "existing": true, + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-05-01", + "subscriptionId": "[variables('existingSubscriptionId')]", + "resourceGroup": "[variables('existingResourceGroupName')]", + "name": "[variables('existingName')]" + }, + "aiSearch": { + "condition": "[empty(parameters('existingResourceId'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.search.search-service.{0}', parameters('name')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "cmkEnforcement": { + "value": "Unspecified" + }, + "managedIdentities": { + "value": { + "systemAssigned": true + } + }, + "publicNetworkAccess": "[if(variables('privateNetworkingEnabled'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "disableLocalAuth": { + "value": "[variables('privateNetworkingEnabled')]" + }, + "authOptions": "[if(variables('privateNetworkingEnabled'), createObject('value', null()), createObject('value', createObject('aadOrApiKey', createObject('aadAuthFailureMode', 'http401WithBearerChallenge'))))]", + "sku": { + "value": "standard" + }, + "partitionCount": { + "value": 1 + }, + "replicaCount": { + "value": 3 + }, + "roleAssignments": { + "value": "[parameters('roleAssignments')]" + }, + "privateEndpoints": "[if(variables('privateNetworkingEnabled'), createObject('value', createArray(createObject('privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', parameters('privateDnsZoneResourceId')))), 'subnetResourceId', parameters('privateEndpointSubnetResourceId')))), createObject('value', createArray()))]", + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10902281417196168235" + }, + "name": "Search Services", + "description": "This module deploys a Search Service." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "secretsExportConfigurationType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The key vault name where to store the API Admin keys generated by the modules." + } + }, + "primaryAdminKeyName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The primaryAdminKey secret name to create." + } + }, + "secondaryAdminKeyName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The secondaryAdminKey secret name to create." + } + } + } + }, + "secretsOutputType": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/definitions/secretSetType", + "metadata": { + "description": "An exported secret's references." + } + } + }, + "authOptionsType": { + "type": "object", + "properties": { + "aadOrApiKey": { + "type": "object", + "properties": { + "aadAuthFailureMode": { + "type": "string", + "allowedValues": [ + "http401WithBearerChallenge", + "http403" + ], + "nullable": true, + "metadata": { + "description": "Optional. Describes what response the data plane API of a search service would send for requests that failed authentication." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Indicates that either the API key or an access token from a Microsoft Entra ID tenant can be used for authentication." + } + }, + "apiKeyOnly": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Indicates that only the API key can be used for authentication." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "networkRuleSetType": { + "type": "object", + "properties": { + "bypass": { + "type": "string", + "allowedValues": [ + "AzurePortal", + "AzureServices", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Network specific rules that determine how the Azure AI Search service may be reached." + } + }, + "ipRules": { + "type": "array", + "items": { + "$ref": "#/definitions/ipRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP restriction rules that defines the inbound network(s) with allowing access to the search service endpoint. At the meantime, all other public IP networks are blocked by the firewall. These restriction rules are applied only when the 'publicNetworkAccess' of the search service is 'enabled'; otherwise, traffic over public interface is not allowed even with any public IP rules, and private endpoint connections would be the exclusive access method." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipRuleType": { + "type": "object", + "properties": { + "value": { + "type": "string", + "metadata": { + "description": "Required. Value corresponding to a single IPv4 address (eg., 123.1.2.3) or an IP range in CIDR format (eg., 123.1.2.3/24) to be allowed." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "_1.lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateEndpointSingleServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private Endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the Private Endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The subresource to deploy the Private Endpoint for. For example \"vault\" for a Key Vault Private Endpoint." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS Zone Group to configure for the Private Endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the Private Endpoint. This will be used to map to the first-party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the Private Endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the Private Endpoint." + } + }, + "lock": { + "$ref": "#/definitions/_1.lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/Resource Groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can be assumed (i.e., for services that only have one Private Endpoint type like 'vault' for key vault).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "secretSetType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "modules/keyVaultExport.bicep" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Azure Cognitive Search service to create or update. Search service names must only contain lowercase letters, digits or dashes, cannot use dash as the first two or last one characters, cannot contain consecutive dashes, and must be between 2 and 60 characters in length. Search service names must be globally unique since they are part of the service URI (https://.search.windows.net). You cannot change the service name after the service is created." + } + }, + "authOptions": { + "$ref": "#/definitions/authOptionsType", + "nullable": true, + "metadata": { + "description": "Optional. Defines the options for how the data plane API of a Search service authenticates requests. Must remain an empty object {} if 'disableLocalAuth' is set to true." + } + }, + "disableLocalAuth": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. When set to true, calls to the search service will not be permitted to utilize API keys for authentication. This cannot be set to true if 'authOptions' are defined." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "cmkEnforcement": { + "type": "string", + "defaultValue": "Unspecified", + "allowedValues": [ + "Disabled", + "Enabled", + "Unspecified" + ], + "metadata": { + "description": "Optional. Describes a policy that determines how resources within the search service are to be encrypted with Customer Managed Keys." + } + }, + "hostingMode": { + "type": "string", + "defaultValue": "default", + "allowedValues": [ + "default", + "highDensity" + ], + "metadata": { + "description": "Optional. Applicable only for the standard3 SKU. You can set this property to enable up to 3 high density partitions that allow up to 1000 indexes, which is much higher than the maximum indexes allowed for any other SKU. For the standard3 SKU, the value is either 'default' or 'highDensity'. For all other SKUs, this value must be 'default'." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings for all Resources in the solution." + } + }, + "networkRuleSet": { + "$ref": "#/definitions/networkRuleSetType", + "nullable": true, + "metadata": { + "description": "Optional. Network specific rules that determine how the Azure Cognitive Search service may be reached." + } + }, + "partitionCount": { + "type": "int", + "defaultValue": 1, + "minValue": 1, + "maxValue": 12, + "metadata": { + "description": "Optional. The number of partitions in the search service; if specified, it can be 1, 2, 3, 4, 6, or 12. Values greater than 1 are only valid for standard SKUs. For 'standard3' services with hostingMode set to 'highDensity', the allowed values are between 1 and 3." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointSingleServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." + } + }, + "sharedPrivateLinkResources": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. The sharedPrivateLinkResources to create as part of the search Service." + } + }, + "publicNetworkAccess": { + "type": "string", + "defaultValue": "Enabled", + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. This value can be set to 'Enabled' to avoid breaking changes on existing customer resources and templates. If set to 'Disabled', traffic over public interface is not allowed, and private endpoint connections would be the exclusive access method." + } + }, + "secretsExportConfiguration": { + "$ref": "#/definitions/secretsExportConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Key vault reference and secret settings for the module's secrets export." + } + }, + "replicaCount": { + "type": "int", + "defaultValue": 3, + "minValue": 1, + "maxValue": 12, + "metadata": { + "description": "Optional. The number of replicas in the search service. If specified, it must be a value between 1 and 12 inclusive for standard SKUs or between 1 and 3 inclusive for basic SKU." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "semanticSearch": { + "type": "string", + "nullable": true, + "allowedValues": [ + "disabled", + "free", + "standard" + ], + "metadata": { + "description": "Optional. Sets options that control the availability of semantic search. This configuration is only possible for certain search SKUs in certain locations." + } + }, + "sku": { + "type": "string", + "defaultValue": "standard", + "allowedValues": [ + "basic", + "free", + "standard", + "standard2", + "standard3", + "storage_optimized_l1", + "storage_optimized_l2" + ], + "metadata": { + "description": "Optional. Defines the SKU of an Azure Cognitive Search Service, which determines price tier and capacity limits." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Search/searchServices@2025-02-01-preview#properties/tags" + }, + "description": "Optional. Tags to help categorize the resource in the Azure portal." + }, + "nullable": true + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', '')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Search Index Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", + "Search Index Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f')]", + "Search Service Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.search-searchservice.{0}.{1}', replace('0.11.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "searchService": { + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-02-01-preview", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('sku')]" + }, + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", + "properties": { + "authOptions": "[parameters('authOptions')]", + "disableLocalAuth": "[parameters('disableLocalAuth')]", + "encryptionWithCmk": { + "enforcement": "[parameters('cmkEnforcement')]" + }, + "hostingMode": "[parameters('hostingMode')]", + "networkRuleSet": "[parameters('networkRuleSet')]", + "partitionCount": "[parameters('partitionCount')]", + "replicaCount": "[parameters('replicaCount')]", + "publicNetworkAccess": "[toLower(parameters('publicNetworkAccess'))]", + "semanticSearch": "[parameters('semanticSearch')]" + } + }, + "searchService_diagnosticSettings": { + "copy": { + "name": "searchService_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "searchService" + ] + }, + "searchService_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "searchService" + ] + }, + "searchService_roleAssignments": { + "copy": { + "name": "searchService_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Search/searchServices', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "searchService" + ] + }, + "searchService_privateEndpoints": { + "copy": { + "name": "searchService_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-searchService-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Search/searchServices', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService')))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService'), copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Search/searchServices', parameters('name')), 'groupIds', createArray(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'service'), 'searchService')), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "searchService" + ] + }, + "searchService_sharedPrivateLinkResources": { + "copy": { + "name": "searchService_sharedPrivateLinkResources", + "count": "[length(parameters('sharedPrivateLinkResources'))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-searchService-SharedPrvLink-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(parameters('sharedPrivateLinkResources')[copyIndex()], 'name'), format('spl-{0}-{1}-{2}', last(split(resourceId('Microsoft.Search/searchServices', parameters('name')), '/')), parameters('sharedPrivateLinkResources')[copyIndex()].groupId, copyIndex()))]" + }, + "searchServiceName": { + "value": "[parameters('name')]" + }, + "privateLinkResourceId": { + "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].privateLinkResourceId]" + }, + "groupId": { + "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].groupId]" + }, + "requestMessage": { + "value": "[parameters('sharedPrivateLinkResources')[copyIndex()].requestMessage]" + }, + "resourceRegion": { + "value": "[tryGet(parameters('sharedPrivateLinkResources')[copyIndex()], 'resourceRegion')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "557730297583881254" + }, + "name": "Search Services Private Link Resources", + "description": "This module deploys a Search Service Private Link Resource." + }, + "parameters": { + "searchServiceName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent searchServices. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the shared private link resource managed by the Azure Cognitive Search service within the specified resource group." + } + }, + "privateLinkResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the resource the shared private link resource is for." + } + }, + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The group ID from the provider of resource the shared private link resource is for." + } + }, + "requestMessage": { + "type": "string", + "metadata": { + "description": "Required. The request message for requesting approval of the shared private link resource." + } + }, + "resourceRegion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Can be used to specify the Azure Resource Manager location of the resource to which a shared private link is to be created. This is only required for those resources whose DNS configuration are regional (such as Azure Kubernetes Service)." + } + } + }, + "resources": { + "searchService": { + "existing": true, + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-02-01-preview", + "name": "[parameters('searchServiceName')]" + }, + "sharedPrivateLinkResource": { + "type": "Microsoft.Search/searchServices/sharedPrivateLinkResources", + "apiVersion": "2025-02-01-preview", + "name": "[format('{0}/{1}', parameters('searchServiceName'), parameters('name'))]", + "properties": { + "privateLinkResourceId": "[parameters('privateLinkResourceId')]", + "groupId": "[parameters('groupId')]", + "requestMessage": "[parameters('requestMessage')]", + "resourceRegion": "[parameters('resourceRegion')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the shared private link resource." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the shared private link resource." + }, + "value": "[resourceId('Microsoft.Search/searchServices/sharedPrivateLinkResources', parameters('searchServiceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the shared private link resource was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "searchService" + ] + }, + "secretsExport": { + "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", + "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" + }, + "secretsToSet": { + "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'primaryAdminKeyName'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'primaryAdminKeyName'), 'value', listAdminKeys('searchService', '2025-02-01-preview').primaryKey)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'secondaryAdminKeyName'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'secondaryAdminKeyName'), 'value', listAdminKeys('searchService', '2025-02-01-preview').secondaryKey)), createArray()))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "7634110751636246703" + } + }, + "definitions": { + "secretSetType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "secretToSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret to set." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret to set." + } + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Key Vault to set the ecrets in." + } + }, + "secretsToSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretToSetType" + }, + "metadata": { + "description": "Required. The secrets to set in the Key Vault." + } + } + }, + "resources": { + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "secrets": { + "copy": { + "name": "secrets", + "count": "[length(parameters('secretsToSet'))]" + }, + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "properties": { + "value": "[parameters('secretsToSet')[copyIndex()].value]" + } + } + }, + "outputs": { + "secretsSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretSetType" + }, + "metadata": { + "description": "The references to the secrets exported to the provided Key Vault." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "input": { + "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", + "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]" + } + } + } + } + } + }, + "dependsOn": [ + "searchService" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the search service." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the search service." + }, + "value": "[resourceId('Microsoft.Search/searchServices', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the search service was created in." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('searchService', '2025-02-01-preview', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('searchService', '2025-02-01-preview', 'full').location]" + }, + "endpoint": { + "type": "string", + "metadata": { + "description": "The endpoint of the search service." + }, + "value": "[reference('searchService').endpoint]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the search service." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('searchService_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + }, + "exportedSecrets": { + "$ref": "#/definitions/secretsOutputType", + "metadata": { + "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." + }, + "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + }, + "primaryKey": { + "type": "securestring", + "metadata": { + "description": "The primary admin API key of the search service." + }, + "value": "[listAdminKeys('searchService', '2025-02-01-preview').primaryKey]" + }, + "secondaryKey": { + "type": "securestring", + "metadata": { + "description": "The secondaryKey admin API key of the search service." + }, + "value": "[listAdminKeys('searchService', '2025-02-01-preview').secondaryKey]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the AI Search resource." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('aiSearch').outputs.name.value, variables('existingName'))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the AI Search resource." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('aiSearch').outputs.resourceId.value, extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingSubscriptionId'), variables('existingResourceGroupName')), 'Microsoft.Search/searchServices', variables('existingName')))]" + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID of the AI Search resource." + }, + "value": "[if(empty(parameters('existingResourceId')), subscription().subscriptionId, variables('existingSubscriptionId'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource Group Name of the AI Search resource." + }, + "value": "[if(empty(parameters('existingResourceId')), resourceGroup().name, variables('existingResourceGroupName'))]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "System assigned managed identity principal ID of the AI Search resource." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('aiSearch').outputs.systemAssignedMIPrincipalId.value, '')]" + } + } + } + } + }, + "storageAccount": { + "condition": "[parameters('includeAssociatedResources')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.storageAccount.{0}', variables('resourcesName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingResourceId": { + "value": "[tryGet(parameters('storageAccountConfiguration'), 'existingResourceId')]" + }, + "name": { + "value": "[take(if(not(empty(tryGet(parameters('storageAccountConfiguration'), 'name'))), parameters('storageAccountConfiguration').name, format('st{0}', variables('resourcesName'))), 24)]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "privateEndpointSubnetResourceId": { + "value": "[parameters('privateEndpointSubnetResourceId')]" + }, + "blobPrivateDnsZoneResourceId": { + "value": "[tryGet(parameters('storageAccountConfiguration'), 'blobPrivateDnsZoneResourceId')]" + }, + "roleAssignments": { + "value": "[concat(if(and(not(empty(parameters('storageAccountConfiguration'))), not(empty(tryGet(parameters('storageAccountConfiguration'), 'roleAssignments')))), parameters('storageAccountConfiguration').roleAssignments, createArray()), createArray(createObject('principalId', reference('foundryAccount').outputs.systemAssignedMIPrincipalId.value, 'principalType', 'ServicePrincipal', 'roleDefinitionIdOrName', 'Storage Blob Data Contributor')), if(empty(tryGet(parameters('aiSearchConfiguration'), 'existingResourceId')), createArray(createObject('principalId', reference('aiSearch').outputs.systemAssignedMIPrincipalId.value, 'principalType', 'ServicePrincipal', 'roleDefinitionIdOrName', 'Storage Blob Data Contributor')), createArray()))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "10412454114821386555" + } + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "functions": [ + { + "namespace": "__bicep", + "members": { + "getResourceGroupName": { + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(greater(length(parameters('parts')), 4), parameters('parts')[4], resourceGroup().name)]" + }, + "metadata": { + "description": "Extracts the Resource Group Name from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getResourceName": { + "parameters": [ + { + "type": "string", + "nullable": true, + "name": "resourceId" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(and(and(not(empty(parameters('resourceId'))), contains(parameters('resourceId'), '/')), not(empty(parameters('parts')))), last(parameters('parts')), coalesce(parameters('resourceId'), ''))]" + }, + "metadata": { + "description": "Extracts the Resource Name from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getResourceParts": { + "parameters": [ + { + "type": "string", + "nullable": true, + "name": "resourceId" + } + ], + "output": { + "type": "array", + "items": { + "type": "string" + }, + "value": "[split(coalesce(parameters('resourceId'), ''), '/')]" + }, + "metadata": { + "description": "Splits Resource ID into its components.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getSubscriptionId": { + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(greater(length(parameters('parts')), 2), parameters('parts')[2], subscription().subscriptionId)]" + }, + "metadata": { + "description": "Extracts the Subscription ID from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + } + } + } + ], + "parameters": { + "name": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Required. The name of the storage account." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Required. The location for the storage account." + } + }, + "existingResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full resource ID of an existing storage account to use instead of creating a new one." + } + }, + "privateEndpointSubnetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource Id of an existing subnet to use for private connectivity. This is required along with 'blobPrivateDnsZoneResourceId' to establish private endpoints." + } + }, + "blobPrivateDnsZoneResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the private DNS zone for the storage account blob service to establish private endpoints." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the role assignments for the storage account." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Resources/resourceGroups@2025-04-01#properties/tags" + }, + "description": "Optional. Specifies the resource tags for all the resources." + }, + "defaultValue": {} + } + }, + "variables": { + "existingResourceParts": "[__bicep.getResourceParts(parameters('existingResourceId'))]", + "existingName": "[__bicep.getResourceName(parameters('existingResourceId'), variables('existingResourceParts'))]", + "existingSubscriptionId": "[__bicep.getSubscriptionId(variables('existingResourceParts'))]", + "existingResourceGroupName": "[__bicep.getResourceGroupName(variables('existingResourceParts'))]", + "privateNetworkingEnabled": "[and(not(empty(parameters('blobPrivateDnsZoneResourceId'))), not(empty(parameters('privateEndpointSubnetResourceId'))))]" + }, + "resources": { + "existingStorageAccount": { + "condition": "[not(empty(parameters('existingResourceId')))]", + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "subscriptionId": "[variables('existingSubscriptionId')]", + "resourceGroup": "[variables('existingResourceGroupName')]", + "name": "[variables('existingName')]" + }, + "storageAccount": { + "condition": "[empty(parameters('existingResourceId'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.storage.storage-account.{0}', parameters('name')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "publicNetworkAccess": "[if(variables('privateNetworkingEnabled'), createObject('value', 'Disabled'), createObject('value', 'Enabled'))]", + "accessTier": { + "value": "Hot" + }, + "allowBlobPublicAccess": { + "value": "[not(variables('privateNetworkingEnabled'))]" + }, + "allowSharedKeyAccess": { + "value": false + }, + "allowCrossTenantReplication": { + "value": false + }, + "blobServices": { + "value": { + "deleteRetentionPolicyEnabled": true, + "deleteRetentionPolicyDays": 7, + "containerDeleteRetentionPolicyEnabled": true, + "containerDeleteRetentionPolicyDays": 7 + } + }, + "minimumTlsVersion": { + "value": "TLS1_2" + }, + "networkAcls": { + "value": { + "defaultAction": "[if(variables('privateNetworkingEnabled'), 'Deny', 'Allow')]", + "bypass": "AzureServices" + } + }, + "supportsHttpsTrafficOnly": { + "value": true + }, + "privateEndpoints": "[if(variables('privateNetworkingEnabled'), createObject('value', createArray(createObject('privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', parameters('blobPrivateDnsZoneResourceId')))), 'service', 'blob', 'subnetResourceId', parameters('privateEndpointSubnetResourceId')))), createObject('value', createArray()))]", + "roleAssignments": { + "value": "[parameters('roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "3530476863855541150" + }, + "name": "Storage Accounts", + "description": "This module deploys a Storage Account." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses of the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the private endpoints output." + } + }, + "networkAclsType": { + "type": "object", + "properties": { + "resourceAccessRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "tenantId": { + "type": "string", + "metadata": { + "description": "Required. The ID of the tenant in which the resource resides in." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the target service. Can also contain a wildcard, if multiple services e.g. in a resource group should be included." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Sets the resource access rules. Array entries must consist of \"tenantId\" and \"resourceId\" fields only." + } + }, + "bypass": { + "type": "string", + "allowedValues": [ + "AzureServices", + "AzureServices, Logging", + "AzureServices, Logging, Metrics", + "AzureServices, Metrics", + "Logging", + "Logging, Metrics", + "Metrics", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether traffic is bypassed for Logging/Metrics/AzureServices. Possible values are any combination of Logging,Metrics,AzureServices (For example, \"Logging, Metrics\"), or None to bypass none of those traffics." + } + }, + "virtualNetworkRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Sets the virtual network rules." + } + }, + "ipRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Sets the IP ACL rules." + } + }, + "defaultAction": { + "type": "string", + "allowedValues": [ + "Allow", + "Deny" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the default action of allow or deny when no other rules match." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the network configuration." + } + }, + "secretsExportConfigurationType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The key vault name where to store the keys and connection strings generated by the modules." + } + }, + "accessKey1Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The accessKey1 secret name to create." + } + }, + "connectionString1Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The connectionString1 secret name to create." + } + }, + "accessKey2Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The accessKey2 secret name to create." + } + }, + "connectionString2Name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The connectionString2 secret name to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of the exported secrets." + } + }, + "localUserType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the local user used for SFTP Authentication." + } + }, + "hasSharedKey": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Indicates whether shared key exists. Set it to false to remove existing shared key." + } + }, + "hasSshKey": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH key exists. Set it to false to remove existing SSH key." + } + }, + "hasSshPassword": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH password exists. Set it to false to remove existing SSH password." + } + }, + "homeDirectory": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The local user home directory." + } + }, + "permissionScopes": { + "type": "array", + "items": { + "$ref": "#/definitions/permissionScopeType" + }, + "metadata": { + "description": "Required. The permission scopes of the local user." + } + }, + "sshAuthorizedKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/sshAuthorizedKeyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The local user SSH authorized keys for SFTP." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a local user." + } + }, + "blobServiceType": { + "type": "object", + "properties": { + "automaticSnapshotPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Automatic Snapshot is enabled if set to true." + } + }, + "changeFeedEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for change feed events. Indicates whether change feed event logging is enabled for the Blob service." + } + }, + "changeFeedRetentionInDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 146000, + "metadata": { + "description": "Optional. Indicates whether change feed event logging is enabled for the Blob service. Indicates the duration of changeFeed retention in days. If left blank, it indicates an infinite retention of the change feed." + } + }, + "containerDeleteRetentionPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for container soft delete. Indicates whether DeleteRetentionPolicy is enabled." + } + }, + "containerDeleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted item should be retained." + } + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "defaultServiceVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Indicates the default version to use for requests to the Blob service if an incoming request's version is not specified. Possible values include version 2008-10-27 and all more recent versions." + } + }, + "deleteRetentionPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for blob soft delete." + } + }, + "deleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted blob should be retained." + } + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "isVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Use versioning to automatically maintain previous versions of your blobs. Cannot be enabled for ADLS Gen2 storage accounts." + } + }, + "lastAccessTimeTrackingPolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service property to configure last access time based tracking policy. When set to true last access time based tracking is enabled." + } + }, + "restorePolicyEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. The blob service properties for blob restore policy. If point-in-time restore is enabled, then versioning, change feed, and blob soft delete must also be enabled." + } + }, + "restorePolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "metadata": { + "description": "Optional. How long this blob can be restored. It should be less than DeleteRetentionPolicy days." + } + }, + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/containerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Blob containers to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a blob service." + } + }, + "_1.immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "metadata": { + "description": "The type for an immutability policy.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/container/main.bicep" + } + } + }, + "_2.secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "_3.lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_3.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_3.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_3.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_3.roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "containerType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/_1.immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "nullable": true + }, + "publicAccess": { + "type": "string", + "allowedValues": [ + "Blob", + "Container", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "description": "The type of a storage container.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/main.bicep" + } + } + }, + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "description": "The type for a cors rule.", + "__bicep_imported_from!": { + "sourceTemplate": "blob-service/main.bicep" + } + } + }, + "customerManagedKeyWithAutoRotateType": { + "type": "object", + "properties": { + "keyVaultResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of a key vault to reference a customer managed key for encryption from." + } + }, + "keyName": { + "type": "string", + "metadata": { + "description": "Required. The name of the customer managed key to use for encryption." + } + }, + "keyVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The version of the customer managed key to reference for encryption. If not provided, using version as per 'autoRotationEnabled' setting." + } + }, + "autoRotationEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable auto-rotating to the latest key version. Default is `true`. If set to `false`, the latest key version at the time of the deployment is used." + } + }, + "userAssignedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User assigned identity to use when fetching the customer managed key. Required if no system assigned identity is available for use." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a customer-managed key. To be used if the resource type supports auto-rotation of the customer-managed key.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingMetricsOnlyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of diagnostic setting." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "permissionScopeType": { + "type": "object", + "properties": { + "permissions": { + "type": "string", + "metadata": { + "description": "Required. The permissions for the local user. Possible values include: Read (r), Write (w), Delete (d), List (l), and Create (c)." + } + }, + "resourceName": { + "type": "string", + "metadata": { + "description": "Required. The name of resource, normally the container name or the file share name, used by the local user." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The service used by the local user, e.g. blob, file." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "local-user/main.bicep" + } + } + }, + "privateEndpointMultiServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the private endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The subresource to deploy the private endpoint for. For example \"blob\", \"table\", \"queue\" or \"file\" for a Storage Account's Private Endpoints." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_3.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "lock": { + "$ref": "#/definitions/_3.lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/_3.roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can NOT be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "secretsOutputType": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/definitions/_2.secretSetOutputType", + "metadata": { + "description": "An exported secret's references." + } + }, + "metadata": { + "description": "A map of the exported secrets", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "sshAuthorizedKeyType": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Description used to store the function/usage of the key." + } + }, + "key": { + "type": "securestring", + "metadata": { + "description": "Required. SSH public key base64 encoded. The format should be: '{keyType} {keyData}', e.g. ssh-rsa AAAABBBB." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "local-user/main.bicep" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Required. Name of the Storage Account. Must be lower-case." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all resources." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "kind": { + "type": "string", + "defaultValue": "StorageV2", + "allowedValues": [ + "Storage", + "StorageV2", + "BlobStorage", + "FileStorage", + "BlockBlobStorage" + ], + "metadata": { + "description": "Optional. Type of Storage Account to create." + } + }, + "skuName": { + "type": "string", + "defaultValue": "Standard_GRS", + "allowedValues": [ + "Standard_LRS", + "Standard_ZRS", + "Standard_GRS", + "Standard_GZRS", + "Standard_RAGRS", + "Standard_RAGZRS", + "StandardV2_LRS", + "StandardV2_ZRS", + "StandardV2_GRS", + "StandardV2_GZRS", + "Premium_LRS", + "Premium_ZRS", + "PremiumV2_LRS", + "PremiumV2_ZRS" + ], + "metadata": { + "description": "Optional. Storage Account Sku Name - note: certain V2 SKUs require the use of: kind = FileStorage." + } + }, + "accessTier": { + "type": "string", + "defaultValue": "Hot", + "allowedValues": [ + "Premium", + "Hot", + "Cool", + "Cold" + ], + "metadata": { + "description": "Conditional. Required if the Storage Account kind is set to BlobStorage. The access tier is used for billing. The \"Premium\" access tier is the default value for premium block blobs storage account type and it cannot be changed for the premium block blobs storage account type." + } + }, + "largeFileSharesState": { + "type": "string", + "defaultValue": "Disabled", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "metadata": { + "description": "Optional. Allow large file shares if set to 'Enabled'. It cannot be disabled once it is enabled. Only supported on locally redundant and zone redundant file shares. It cannot be set on FileStorage storage accounts (storage accounts for premium file shares)." + } + }, + "azureFilesIdentityBasedAuthentication": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts@2024-01-01#properties/properties/properties/azureFilesIdentityBasedAuthentication" + }, + "description": "Optional. Provides the identity based authentication settings for Azure Files." + }, + "nullable": true + }, + "defaultToOAuthAuthentication": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. A boolean flag which indicates whether the default authentication is OAuth or not." + } + }, + "allowSharedKeyAccess": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether the storage account permits requests to be authorized with the account access key via Shared Key. If false, then all requests, including shared access signatures, must be authorized with Azure Active Directory (Azure AD). The default value is null, which is equivalent to true." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointMultiServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible." + } + }, + "managementPolicyRules": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. The Storage Account ManagementPolicies Rules." + } + }, + "networkAcls": { + "$ref": "#/definitions/networkAclsType", + "nullable": true, + "metadata": { + "description": "Optional. Networks ACLs, this value contains IPs to whitelist and/or Subnet information. If in use, bypass needs to be supplied. For security reasons, it is recommended to set the DefaultAction Deny." + } + }, + "requireInfrastructureEncryption": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. A Boolean indicating whether or not the service applies a secondary layer of encryption with platform managed keys for data at rest. For security reasons, it is recommended to set it to true." + } + }, + "allowCrossTenantReplication": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Allow or disallow cross AAD tenant object replication." + } + }, + "customDomainName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. Sets the custom domain name assigned to the storage account. Name is the CNAME source." + } + }, + "customDomainUseSubDomainName": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether indirect CName validation is enabled. This should only be set on updates." + } + }, + "dnsEndpointType": { + "type": "string", + "nullable": true, + "allowedValues": [ + "AzureDnsZone", + "Standard" + ], + "metadata": { + "description": "Optional. Allows you to specify the type of endpoint. Set this to AzureDNSZone to create a large number of accounts in a single subscription, which creates accounts in an Azure DNS Zone and the endpoint URL will have an alphanumeric DNS Zone identifier." + } + }, + "blobServices": { + "$ref": "#/definitions/blobServiceType", + "defaultValue": "[if(not(equals(parameters('kind'), 'FileStorage')), createObject('containerDeleteRetentionPolicyEnabled', true(), 'containerDeleteRetentionPolicyDays', 7, 'deleteRetentionPolicyEnabled', true(), 'deleteRetentionPolicyDays', 6), createObject())]", + "metadata": { + "description": "Optional. Blob service and containers to deploy." + } + }, + "fileServices": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. File service and shares to deploy." + } + }, + "queueServices": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Queue service and queues to create." + } + }, + "tableServices": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Table service and tables to create." + } + }, + "allowBlobPublicAccess": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether public access is enabled for all blobs or containers in the storage account. For security reasons, it is recommended to set it to false." + } + }, + "minimumTlsVersion": { + "type": "string", + "defaultValue": "TLS1_2", + "allowedValues": [ + "TLS1_2" + ], + "metadata": { + "description": "Optional. Set the minimum TLS version on request to storage. The TLS versions 1.0 and 1.1 are deprecated and not supported anymore." + } + }, + "enableHierarchicalNamespace": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Conditional. If true, enables Hierarchical Namespace for the storage account. Required if enableSftp or enableNfsV3 is set to true." + } + }, + "enableSftp": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. If true, enables Secure File Transfer Protocol for the storage account. Requires enableHierarchicalNamespace to be true." + } + }, + "localUsers": { + "type": "array", + "items": { + "$ref": "#/definitions/localUserType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Local users to deploy for SFTP authentication." + } + }, + "isLocalUserEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables local users feature, if set to true." + } + }, + "enableNfsV3": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. If true, enables NFS 3.0 support for the storage account. Requires enableHierarchicalNamespace to be true." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts@2024-01-01#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "allowedCopyScope": { + "type": "string", + "nullable": true, + "allowedValues": [ + "AAD", + "PrivateLink" + ], + "metadata": { + "description": "Optional. Restrict copy to and from Storage Accounts within an AAD tenant or with Private Links to the same VNet." + } + }, + "publicNetworkAccess": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Enabled", + "Disabled" + ], + "metadata": { + "description": "Optional. Whether or not public network access is allowed for this resource. For security reasons it should be disabled. If not specified, it will be disabled by default if private endpoints are set and networkAcls are not set." + } + }, + "supportsHttpsTrafficOnly": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Allows HTTPS traffic only to storage service if sets to true." + } + }, + "customerManagedKey": { + "$ref": "#/definitions/customerManagedKeyWithAutoRotateType", + "nullable": true, + "metadata": { + "description": "Optional. The customer managed key definition." + } + }, + "sasExpirationPeriod": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The SAS expiration period. DD.HH:MM:SS." + } + }, + "sasExpirationAction": { + "type": "string", + "defaultValue": "Log", + "allowedValues": [ + "Block", + "Log" + ], + "metadata": { + "description": "Optional. The SAS expiration action. Allowed values are Block and Log." + } + }, + "keyType": { + "type": "string", + "nullable": true, + "allowedValues": [ + "Account", + "Service" + ], + "metadata": { + "description": "Optional. The keyType to use with Queue & Table services." + } + }, + "secretsExportConfiguration": { + "$ref": "#/definitions/secretsExportConfigurationType", + "nullable": true, + "metadata": { + "description": "Optional. Key vault reference and secret settings for the module's secrets export." + } + }, + "immutableStorageWithVersioning": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts@2025-01-01#properties/properties/properties/immutableStorageWithVersioning" + }, + "description": "Optional. The property is immutable and can only be set to true at the account creation time. When set to true, it enables object level immutability for all the new containers in the account by default. Cannot be enabled for ADLS Gen2 storage accounts." + }, + "nullable": true + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "immutabilityValidation": "[if(and(equals(parameters('enableHierarchicalNamespace'), true()), not(empty(parameters('immutableStorageWithVersioning')))), fail('Configuration error: Immutable storage with versioning cannot be enabled when hierarchical namespace is enabled.'), null())]", + "supportsBlobService": "[or(or(or(equals(parameters('kind'), 'BlockBlobStorage'), equals(parameters('kind'), 'BlobStorage')), equals(parameters('kind'), 'StorageV2')), equals(parameters('kind'), 'Storage'))]", + "supportsFileService": "[or(or(equals(parameters('kind'), 'FileStorage'), equals(parameters('kind'), 'StorageV2')), equals(parameters('kind'), 'Storage'))]", + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Blob Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "Storage Blob Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "Storage Blob Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", + "Storage Blob Delegator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a')]", + "Storage File Data Privileged Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '69566ab7-960f-475b-8e7c-b3118f30c6bd')]", + "Storage File Data Privileged Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b8eda974-7b85-4f76-af95-65846b26df6d')]", + "Storage File Data SMB Share Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')]", + "Storage File Data SMB Share Elevated Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7264617-510b-434b-a828-9731dc254ea7')]", + "Storage File Data SMB Share Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'aba4ae5f-2193-4029-9191-0cb91df5e314')]", + "Storage Queue Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "Storage Queue Data Message Processor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')]", + "Storage Queue Data Message Sender": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]", + "Storage Queue Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '19e7f393-937e-4f77-808e-94535e297925')]", + "Storage Table Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "Storage Table Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76199698-9eea-4c19-bc75-cec21354c6b6')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "cMKKeyVault::cMKKey": { + "condition": "[and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), and(not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'))), not(empty(tryGet(parameters('customerManagedKey'), 'keyName')))))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults/keys", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[format('{0}/{1}', last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')), tryGet(parameters('customerManagedKey'), 'keyName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.storage-storageaccount.{0}.{1}', replace('0.27.1', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "cMKKeyVault": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId')))]", + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'keyVaultResourceId'), '/'))]" + }, + "cMKUserAssignedIdentity": { + "condition": "[not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId')))]", + "existing": true, + "type": "Microsoft.ManagedIdentity/userAssignedIdentities", + "apiVersion": "2024-11-30", + "subscriptionId": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]]", + "name": "[last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))]" + }, + "storageAccount": { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "kind": "[parameters('kind')]", + "sku": { + "name": "[parameters('skuName')]" + }, + "identity": "[variables('identity')]", + "tags": "[parameters('tags')]", + "properties": "[shallowMerge(createArray(createObject('allowSharedKeyAccess', parameters('allowSharedKeyAccess'), 'defaultToOAuthAuthentication', parameters('defaultToOAuthAuthentication'), 'allowCrossTenantReplication', parameters('allowCrossTenantReplication'), 'allowedCopyScope', parameters('allowedCopyScope'), 'customDomain', createObject('name', parameters('customDomainName'), 'useSubDomainName', parameters('customDomainUseSubDomainName')), 'dnsEndpointType', parameters('dnsEndpointType'), 'isLocalUserEnabled', parameters('isLocalUserEnabled'), 'encryption', union(createObject('keySource', if(not(empty(parameters('customerManagedKey'))), 'Microsoft.Keyvault', 'Microsoft.Storage'), 'services', createObject('blob', if(variables('supportsBlobService'), createObject('enabled', true()), null()), 'file', if(variables('supportsFileService'), createObject('enabled', true()), null()), 'table', createObject('enabled', true(), 'keyType', parameters('keyType')), 'queue', createObject('enabled', true(), 'keyType', parameters('keyType'))), 'keyvaultproperties', if(not(empty(parameters('customerManagedKey'))), createObject('keyname', parameters('customerManagedKey').keyName, 'keyvaulturi', reference('cMKKeyVault').vaultUri, 'keyversion', if(not(empty(tryGet(parameters('customerManagedKey'), 'keyVersion'))), parameters('customerManagedKey').keyVersion, if(coalesce(tryGet(parameters('customerManagedKey'), 'autoRotationEnabled'), true()), null(), last(split(reference('cMKKeyVault::cMKKey').keyUriWithVersion, '/'))))), null()), 'identity', createObject('userAssignedIdentity', if(not(empty(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'))), extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[2], split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/')[4]), 'Microsoft.ManagedIdentity/userAssignedIdentities', last(split(tryGet(parameters('customerManagedKey'), 'userAssignedIdentityResourceId'), '/'))), null()))), if(parameters('requireInfrastructureEncryption'), createObject('requireInfrastructureEncryption', if(not(equals(parameters('kind'), 'Storage')), parameters('requireInfrastructureEncryption'), null())), createObject())), 'accessTier', if(and(not(equals(parameters('kind'), 'Storage')), not(equals(parameters('kind'), 'BlockBlobStorage'))), parameters('accessTier'), null()), 'sasPolicy', if(not(empty(parameters('sasExpirationPeriod'))), createObject('expirationAction', parameters('sasExpirationAction'), 'sasExpirationPeriod', parameters('sasExpirationPeriod')), null()), 'supportsHttpsTrafficOnly', parameters('supportsHttpsTrafficOnly'), 'isSftpEnabled', parameters('enableSftp'), 'isNfsV3Enabled', if(parameters('enableNfsV3'), parameters('enableNfsV3'), ''), 'largeFileSharesState', if(or(equals(parameters('skuName'), 'Standard_LRS'), equals(parameters('skuName'), 'Standard_ZRS')), parameters('largeFileSharesState'), null()), 'minimumTlsVersion', parameters('minimumTlsVersion'), 'networkAcls', if(not(empty(parameters('networkAcls'))), union(createObject('resourceAccessRules', tryGet(parameters('networkAcls'), 'resourceAccessRules'), 'defaultAction', coalesce(tryGet(parameters('networkAcls'), 'defaultAction'), 'Deny'), 'virtualNetworkRules', tryGet(parameters('networkAcls'), 'virtualNetworkRules'), 'ipRules', tryGet(parameters('networkAcls'), 'ipRules')), if(contains(parameters('networkAcls'), 'bypass'), createObject('bypass', tryGet(parameters('networkAcls'), 'bypass')), createObject())), createObject('bypass', 'AzureServices', 'defaultAction', 'Deny')), 'allowBlobPublicAccess', parameters('allowBlobPublicAccess'), 'publicNetworkAccess', if(not(empty(parameters('publicNetworkAccess'))), parameters('publicNetworkAccess'), if(and(not(empty(parameters('privateEndpoints'))), empty(parameters('networkAcls'))), 'Disabled', null()))), if(not(empty(parameters('azureFilesIdentityBasedAuthentication'))), createObject('azureFilesIdentityBasedAuthentication', parameters('azureFilesIdentityBasedAuthentication')), createObject()), if(not(equals(parameters('enableHierarchicalNamespace'), null())), createObject('isHnsEnabled', parameters('enableHierarchicalNamespace')), createObject()), createObject('immutableStorageWithVersioning', parameters('immutableStorageWithVersioning'))))]", + "dependsOn": [ + "cMKKeyVault", + "cMKKeyVault::cMKKey" + ] + }, + "storageAccount_diagnosticSettings": { + "copy": { + "name": "storageAccount_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_roleAssignments": { + "copy": { + "name": "storageAccount_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_privateEndpoints": { + "copy": { + "name": "storageAccount_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sa-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Storage/storageAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.Storage/storageAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.Storage/storageAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_managementPolicies": { + "condition": "[not(empty(coalesce(parameters('managementPolicyRules'), createArray())))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-ManagementPolicies', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "rules": { + "value": "[parameters('managementPolicyRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "14529265638306912023" + }, + "name": "Storage Account Management Policies", + "description": "This module deploys a Storage Account Management Policy." + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "rules": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/managementPolicies@2024-01-01#properties/properties/properties/policy/properties/rules" + }, + "description": "Required. The Storage Account ManagementPolicies Rules." + } + } + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/managementPolicies", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]", + "properties": { + "policy": { + "rules": "[parameters('rules')]" + } + } + } + ], + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed management policy." + }, + "value": "default" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed management policy." + }, + "value": "default" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed management policy." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount", + "storageAccount_blobServices" + ] + }, + "storageAccount_localUsers": { + "copy": { + "name": "storageAccount_localUsers", + "count": "[length(coalesce(parameters('localUsers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-LocalUsers-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].name]" + }, + "hasSshKey": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].hasSshKey]" + }, + "hasSshPassword": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].hasSshPassword]" + }, + "permissionScopes": { + "value": "[coalesce(parameters('localUsers'), createArray())[copyIndex()].permissionScopes]" + }, + "hasSharedKey": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'hasSharedKey')]" + }, + "homeDirectory": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'homeDirectory')]" + }, + "sshAuthorizedKeys": { + "value": "[tryGet(coalesce(parameters('localUsers'), createArray())[copyIndex()], 'sshAuthorizedKeys')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "3261275799710495788" + }, + "name": "Storage Account Local Users", + "description": "This module deploys a Storage Account Local User, which is used for SFTP authentication." + }, + "definitions": { + "sshAuthorizedKeyType": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Description used to store the function/usage of the key." + } + }, + "key": { + "type": "securestring", + "metadata": { + "description": "Required. SSH public key base64 encoded. The format should be: '{keyType} {keyData}', e.g. ssh-rsa AAAABBBB." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "permissionScopeType": { + "type": "object", + "properties": { + "permissions": { + "type": "string", + "metadata": { + "description": "Required. The permissions for the local user. Possible values include: Read (r), Write (w), Delete (d), List (l), and Create (c)." + } + }, + "resourceName": { + "type": "string", + "metadata": { + "description": "Required. The name of resource, normally the container name or the file share name, used by the local user." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The service used by the local user, e.g. blob, file." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the local user used for SFTP Authentication." + } + }, + "hasSharedKey": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Indicates whether shared key exists. Set it to false to remove existing shared key." + } + }, + "hasSshKey": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH key exists. Set it to false to remove existing SSH key." + } + }, + "hasSshPassword": { + "type": "bool", + "metadata": { + "description": "Required. Indicates whether SSH password exists. Set it to false to remove existing SSH password." + } + }, + "homeDirectory": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The local user home directory." + } + }, + "permissionScopes": { + "type": "array", + "items": { + "$ref": "#/definitions/permissionScopeType" + }, + "metadata": { + "description": "Required. The permission scopes of the local user." + } + }, + "sshAuthorizedKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/sshAuthorizedKeyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The local user SSH authorized keys for SFTP." + } + } + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "localUsers": { + "type": "Microsoft.Storage/storageAccounts/localUsers", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "properties": { + "hasSharedKey": "[parameters('hasSharedKey')]", + "hasSshKey": "[parameters('hasSshKey')]", + "hasSshPassword": "[parameters('hasSshPassword')]", + "homeDirectory": "[parameters('homeDirectory')]", + "permissionScopes": "[parameters('permissionScopes')]", + "sshAuthorizedKeys": "[parameters('sshAuthorizedKeys')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed local user." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed local user." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed local user." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/localUsers', parameters('storageAccountName'), parameters('name'))]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_blobServices": { + "condition": "[not(empty(parameters('blobServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-BlobServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "containers": { + "value": "[tryGet(parameters('blobServices'), 'containers')]" + }, + "automaticSnapshotPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'automaticSnapshotPolicyEnabled')]" + }, + "changeFeedEnabled": { + "value": "[tryGet(parameters('blobServices'), 'changeFeedEnabled')]" + }, + "changeFeedRetentionInDays": { + "value": "[tryGet(parameters('blobServices'), 'changeFeedRetentionInDays')]" + }, + "containerDeleteRetentionPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyEnabled')]" + }, + "containerDeleteRetentionPolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyDays')]" + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "value": "[tryGet(parameters('blobServices'), 'containerDeleteRetentionPolicyAllowPermanentDelete')]" + }, + "corsRules": { + "value": "[tryGet(parameters('blobServices'), 'corsRules')]" + }, + "defaultServiceVersion": { + "value": "[tryGet(parameters('blobServices'), 'defaultServiceVersion')]" + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyAllowPermanentDelete')]" + }, + "deleteRetentionPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyEnabled')]" + }, + "deleteRetentionPolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'deleteRetentionPolicyDays')]" + }, + "isVersioningEnabled": { + "value": "[tryGet(parameters('blobServices'), 'isVersioningEnabled')]" + }, + "lastAccessTimeTrackingPolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'lastAccessTimeTrackingPolicyEnabled')]" + }, + "restorePolicyEnabled": { + "value": "[tryGet(parameters('blobServices'), 'restorePolicyEnabled')]" + }, + "restorePolicyDays": { + "value": "[tryGet(parameters('blobServices'), 'restorePolicyDays')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('blobServices'), 'diagnosticSettings')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9925173705553594819" + }, + "name": "Storage Account blob Services", + "description": "This module deploys a Storage Account Blob Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "containerType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "nullable": true + }, + "publicAccess": { + "type": "string", + "allowedValues": [ + "Blob", + "Container", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type of a storage container." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "metadata": { + "description": "The type for an immutability policy.", + "__bicep_imported_from!": { + "sourceTemplate": "container/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "automaticSnapshotPolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Automatic Snapshot is enabled if set to true." + } + }, + "changeFeedEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service properties for change feed events. Indicates whether change feed event logging is enabled for the Blob service." + } + }, + "changeFeedRetentionInDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 146000, + "metadata": { + "description": "Optional. Indicates whether change feed event logging is enabled for the Blob service. Indicates the duration of changeFeed retention in days. If left blank, it indicates an infinite retention of the change feed." + } + }, + "containerDeleteRetentionPolicyEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The blob service properties for container soft delete. Indicates whether DeleteRetentionPolicy is enabled." + } + }, + "containerDeleteRetentionPolicyDays": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted item should be retained." + } + }, + "containerDeleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "defaultServiceVersion": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Indicates the default version to use for requests to the Blob service if an incoming request's version is not specified. Possible values include version 2008-10-27 and all more recent versions." + } + }, + "deleteRetentionPolicyEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. The blob service properties for blob soft delete." + } + }, + "deleteRetentionPolicyDays": { + "type": "int", + "defaultValue": 7, + "minValue": 1, + "maxValue": 365, + "metadata": { + "description": "Optional. Indicates the number of days that the deleted blob should be retained." + } + }, + "deleteRetentionPolicyAllowPermanentDelete": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This property when set to true allows deletion of the soft deleted blob versions and snapshots. This property cannot be used with blob restore policy. This property only applies to blob service and does not apply to containers or file share." + } + }, + "isVersioningEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Use versioning to automatically maintain previous versions of your blobs. Cannot be enabled for ADLS Gen2 storage accounts." + } + }, + "lastAccessTimeTrackingPolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service property to configure last access time based tracking policy. When set to true last access time based tracking is enabled." + } + }, + "restorePolicyEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. The blob service properties for blob restore policy. If point-in-time restore is enabled, then versioning, change feed, and blob soft delete must also be enabled." + } + }, + "restorePolicyDays": { + "type": "int", + "defaultValue": 7, + "minValue": 1, + "metadata": { + "description": "Optional. How long this blob can be restored. It should be less than DeleteRetentionPolicy days." + } + }, + "containers": { + "type": "array", + "items": { + "$ref": "#/definitions/containerType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Blob containers to create." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "enableReferencedModulesTelemetry": false, + "name": "default" + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "blobServices": { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "properties": { + "automaticSnapshotPolicyEnabled": "[parameters('automaticSnapshotPolicyEnabled')]", + "changeFeed": "[if(parameters('changeFeedEnabled'), createObject('enabled', true(), 'retentionInDays', parameters('changeFeedRetentionInDays')), null())]", + "containerDeleteRetentionPolicy": { + "enabled": "[parameters('containerDeleteRetentionPolicyEnabled')]", + "days": "[parameters('containerDeleteRetentionPolicyDays')]", + "allowPermanentDelete": "[if(equals(parameters('containerDeleteRetentionPolicyEnabled'), true()), parameters('containerDeleteRetentionPolicyAllowPermanentDelete'), null())]" + }, + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]", + "defaultServiceVersion": "[parameters('defaultServiceVersion')]", + "deleteRetentionPolicy": { + "enabled": "[parameters('deleteRetentionPolicyEnabled')]", + "days": "[parameters('deleteRetentionPolicyDays')]", + "allowPermanentDelete": "[if(and(parameters('deleteRetentionPolicyEnabled'), parameters('deleteRetentionPolicyAllowPermanentDelete')), true(), null())]" + }, + "isVersioningEnabled": "[parameters('isVersioningEnabled')]", + "lastAccessTimeTrackingPolicy": "[if(not(equals(reference('storageAccount', '2025-01-01', 'full').kind, 'Storage')), createObject('enable', parameters('lastAccessTimeTrackingPolicyEnabled'), 'name', if(equals(parameters('lastAccessTimeTrackingPolicyEnabled'), true()), 'AccessTimeTracking', null()), 'trackingGranularityInDays', if(equals(parameters('lastAccessTimeTrackingPolicyEnabled'), true()), 1, null())), null())]", + "restorePolicy": "[if(parameters('restorePolicyEnabled'), createObject('enabled', true(), 'days', parameters('restorePolicyDays')), null())]" + }, + "dependsOn": [ + "storageAccount" + ] + }, + "blobServices_diagnosticSettings": { + "copy": { + "name": "blobServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "blobServices" + ] + }, + "blobServices_container": { + "copy": { + "name": "blobServices_container", + "count": "[length(coalesce(parameters('containers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Container-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "blobServiceName": { + "value": "[variables('name')]" + }, + "name": { + "value": "[coalesce(parameters('containers'), createArray())[copyIndex()].name]" + }, + "defaultEncryptionScope": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'defaultEncryptionScope')]" + }, + "denyEncryptionScopeOverride": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'denyEncryptionScopeOverride')]" + }, + "enableNfsV3AllSquash": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'enableNfsV3AllSquash')]" + }, + "enableNfsV3RootSquash": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'enableNfsV3RootSquash')]" + }, + "immutableStorageWithVersioningEnabled": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'immutableStorageWithVersioningEnabled')]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'metadata')]" + }, + "publicAccess": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'publicAccess')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "immutabilityPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'immutabilityPolicy')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "5026123498979497429" + }, + "name": "Storage Account Blob Containers", + "description": "This module deploys a Storage Account Blob Container." + }, + "definitions": { + "immutabilityPolicyType": { + "type": "object", + "properties": { + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an immutability policy." + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "blobServiceName": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the parent Blob Service. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the Storage Container to deploy." + } + }, + "defaultEncryptionScope": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Default the container to use specified encryption scope for all writes." + } + }, + "denyEncryptionScopeOverride": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Block override of encryption scope from the container default." + } + }, + "enableNfsV3AllSquash": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable NFSv3 all squash on blob container." + } + }, + "enableNfsV3RootSquash": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enable NFSv3 root squash on blob container." + } + }, + "immutableStorageWithVersioningEnabled": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. This is an immutable property, when set to true it enables object level immutability at the container level. The property is immutable and can only be set to true at the container creation time. Existing containers must undergo a migration process." + } + }, + "immutabilityPolicy": { + "$ref": "#/definitions/immutabilityPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Configure immutability policy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/blobServices/containers@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair to associate with the container as metadata." + }, + "defaultValue": {} + }, + "publicAccess": { + "type": "string", + "defaultValue": "None", + "allowedValues": [ + "Container", + "Blob", + "None" + ], + "metadata": { + "description": "Optional. Specifies whether data in the container may be accessed publicly and the level of access." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Blob Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "Storage Blob Data Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "Storage Blob Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1')]", + "Storage Blob Delegator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db58b8e5-c6ad-4a2a-8342-4190687cbf4a')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::blobServices": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('blobServiceName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.storage-blobcontainer.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "name": "[parameters('storageAccountName')]" + }, + "container": { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]", + "properties": { + "defaultEncryptionScope": "[parameters('defaultEncryptionScope')]", + "denyEncryptionScopeOverride": "[parameters('denyEncryptionScopeOverride')]", + "enableNfsV3AllSquash": "[if(equals(parameters('enableNfsV3AllSquash'), true()), parameters('enableNfsV3AllSquash'), null())]", + "enableNfsV3RootSquash": "[if(equals(parameters('enableNfsV3RootSquash'), true()), parameters('enableNfsV3RootSquash'), null())]", + "immutableStorageWithVersioning": "[if(parameters('immutableStorageWithVersioningEnabled'), createObject('enabled', parameters('immutableStorageWithVersioningEnabled')), null())]", + "metadata": "[parameters('metadata')]", + "publicAccess": "[parameters('publicAccess')]" + } + }, + "container_roleAssignments": { + "copy": { + "name": "container_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/blobServices/{1}/containers/{2}', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "container" + ] + }, + "container_immutabilityPolicy": { + "condition": "[not(empty(coalesce(parameters('immutabilityPolicy'), createObject())))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[take(format('{0}-ImmutPol', deployment().name), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "containerName": { + "value": "[parameters('name')]" + }, + "immutabilityPeriodSinceCreationInDays": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'immutabilityPeriodSinceCreationInDays')]" + }, + "allowProtectedAppendWrites": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'allowProtectedAppendWrites')]" + }, + "allowProtectedAppendWritesAll": { + "value": "[tryGet(parameters('immutabilityPolicy'), 'allowProtectedAppendWritesAll')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10782942397325758470" + }, + "name": "Storage Account Blob Container Immutability Policies", + "description": "This module deploys a Storage Account Blob Container Immutability Policy." + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "containerName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent container to apply the policy to. Required if the template is used in a standalone deployment." + } + }, + "immutabilityPeriodSinceCreationInDays": { + "type": "int", + "defaultValue": 365, + "metadata": { + "description": "Optional. The immutability period for the blobs in the container since the policy creation, in days." + } + }, + "allowProtectedAppendWrites": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to an append blob while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + }, + "allowProtectedAppendWritesAll": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. This property can only be changed for unlocked time-based retention policies. When enabled, new blocks can be written to both \"Append and Block Blobs\" while maintaining immutability protection and compliance. Only new blocks can be added and any existing blocks cannot be modified or deleted. This property cannot be changed with ExtendImmutabilityPolicy API. The \"allowProtectedAppendWrites\" and \"allowProtectedAppendWritesAll\" properties are mutually exclusive." + } + } + }, + "variables": { + "name": "default" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies", + "apiVersion": "2025-01-01", + "name": "[format('{0}/{1}/{2}/{3}', parameters('storageAccountName'), 'default', parameters('containerName'), variables('name'))]", + "properties": { + "immutabilityPeriodSinceCreationInDays": "[parameters('immutabilityPeriodSinceCreationInDays')]", + "allowProtectedAppendWrites": "[parameters('allowProtectedAppendWrites')]", + "allowProtectedAppendWritesAll": "[parameters('allowProtectedAppendWritesAll')]" + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed immutability policy." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed immutability policy." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies', parameters('storageAccountName'), 'default', parameters('containerName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed immutability policy." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "container" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed container." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed container." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices/containers', parameters('storageAccountName'), parameters('blobServiceName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed container." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "blobServices" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed blob service." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed blob service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/blobServices', parameters('storageAccountName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the deployed blob service." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_fileServices": { + "condition": "[not(empty(parameters('fileServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-FileServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('fileServices'), 'diagnosticSettings')]" + }, + "protocolSettings": { + "value": "[tryGet(parameters('fileServices'), 'protocolSettings')]" + }, + "shareDeleteRetentionPolicy": { + "value": "[tryGet(parameters('fileServices'), 'shareDeleteRetentionPolicy')]" + }, + "shares": { + "value": "[tryGet(parameters('fileServices'), 'shares')]" + }, + "corsRules": { + "value": "[tryGet(parameters('queueServices'), 'corsRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "2735186993322606805" + }, + "name": "Storage Account File Share Services", + "description": "This module deploys a Storage Account File Share Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the file service." + } + }, + "protocolSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/protocolSettings" + }, + "description": "Optional. Protocol settings for file service." + }, + "defaultValue": {} + }, + "shareDeleteRetentionPolicy": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/fileServices@2024-01-01#properties/properties/properties/shareDeleteRetentionPolicy" + }, + "description": "Optional. The service properties for soft delete." + }, + "defaultValue": { + "enabled": true, + "days": 7 + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + }, + "shares": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. File shares to create." + } + } + }, + "variables": { + "enableReferencedModulesTelemetry": false + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "fileServices": { + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('name'))]", + "properties": { + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]", + "protocolSettings": "[parameters('protocolSettings')]", + "shareDeleteRetentionPolicy": "[parameters('shareDeleteRetentionPolicy')]" + } + }, + "fileServices_diagnosticSettings": { + "copy": { + "name": "fileServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/fileServices/{1}', parameters('storageAccountName'), parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "fileServices" + ] + }, + "fileServices_shares": { + "copy": { + "name": "fileServices_shares", + "count": "[length(coalesce(parameters('shares'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-shares-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "fileServicesName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('shares'), createArray())[copyIndex()].name]" + }, + "accessTier": { + "value": "[coalesce(tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'accessTier'), if(equals(reference('storageAccount', '2024-01-01', 'full').kind, 'FileStorage'), 'Premium', 'TransactionOptimized'))]" + }, + "enabledProtocols": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'enabledProtocols')]" + }, + "rootSquash": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'rootSquash')]" + }, + "shareQuota": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'shareQuota')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('shares'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "15881640847294537074" + }, + "name": "Storage Account File Shares", + "description": "This module deploys a Storage Account File Share." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "fileServicesName": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Conditional. The name of the parent file service. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the file share to create." + } + }, + "accessTier": { + "type": "string", + "defaultValue": "TransactionOptimized", + "allowedValues": [ + "Premium", + "Hot", + "Cool", + "TransactionOptimized" + ], + "metadata": { + "description": "Conditional. Access tier for specific share. Required if the Storage Account kind is set to FileStorage (should be set to \"Premium\"). GpV2 account can choose between TransactionOptimized (default), Hot, and Cool." + } + }, + "shareQuota": { + "type": "int", + "defaultValue": 5120, + "metadata": { + "description": "Optional. The maximum size of the share, in gigabytes. Must be greater than 0, and less than or equal to 5120 (5TB). For Large File Shares, the maximum size is 102400 (100TB)." + } + }, + "enabledProtocols": { + "type": "string", + "defaultValue": "SMB", + "allowedValues": [ + "NFS", + "SMB" + ], + "metadata": { + "description": "Optional. The authentication protocol that is used for the file share. Can only be specified when creating a share." + } + }, + "rootSquash": { + "type": "string", + "defaultValue": "NoRootSquash", + "allowedValues": [ + "AllSquash", + "NoRootSquash", + "RootSquash" + ], + "metadata": { + "description": "Optional. Permissions for NFS file shares are enforced by the client OS rather than the Azure Files service. Toggling the root squash behavior reduces the rights of the root user for NFS shares." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage File Data SMB Share Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0c867c2a-1d8c-454a-a3db-ab2ea1bdc8bb')]", + "Storage File Data SMB Share Elevated Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a7264617-510b-434b-a828-9731dc254ea7')]", + "Storage File Data SMB Share Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'aba4ae5f-2193-4029-9191-0cb91df5e314')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::fileService": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), parameters('fileServicesName'))]" + }, + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.storage-fileshare.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "fileShare": { + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name'))]", + "properties": { + "accessTier": "[parameters('accessTier')]", + "shareQuota": "[parameters('shareQuota')]", + "rootSquash": "[if(equals(parameters('enabledProtocols'), 'NFS'), parameters('rootSquash'), null())]", + "enabledProtocols": "[parameters('enabledProtocols')]" + } + }, + "fileShare_roleAssignments": { + "copy": { + "name": "fileShare_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Share-Rbac-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "scope": { + "value": "[replace(resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name')), '/shares/', '/fileshares/')]" + }, + "name": { + "value": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]" + }, + "roleDefinitionId": { + "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" + }, + "principalId": { + "value": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "principalType": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]" + }, + "condition": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]" + }, + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), createObject('value', coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0')), createObject('value', null()))]", + "delegatedManagedIdentityResourceId": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "description": { + "value": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "scope": { + "type": "string", + "metadata": { + "description": "Required. The scope to deploy the role assignment to." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the role assignment." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The role definition Id to assign." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User", + "" + ], + "defaultValue": "", + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"" + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "defaultValue": "2.0", + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[parameters('scope')]", + "name": "[parameters('name')]", + "properties": { + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "principalId": "[parameters('principalId')]", + "description": "[parameters('description')]", + "principalType": "[if(not(empty(parameters('principalType'))), parameters('principalType'), null())]", + "condition": "[if(not(empty(parameters('condition'))), parameters('condition'), null())]", + "conditionVersion": "[if(and(not(empty(parameters('conditionVersion'))), not(empty(parameters('condition')))), parameters('conditionVersion'), null())]", + "delegatedManagedIdentityResourceId": "[if(not(empty(parameters('delegatedManagedIdentityResourceId'))), parameters('delegatedManagedIdentityResourceId'), null())]" + } + } + ] + } + }, + "dependsOn": [ + "fileShare" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/fileServices/shares', parameters('storageAccountName'), parameters('fileServicesName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "fileServices", + "storageAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share service." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/fileServices', parameters('storageAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share service." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_queueServices": { + "condition": "[not(empty(parameters('queueServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-QueueServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('queueServices'), 'diagnosticSettings')]" + }, + "queues": { + "value": "[tryGet(parameters('queueServices'), 'queues')]" + }, + "corsRules": { + "value": "[tryGet(parameters('queueServices'), 'corsRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "1100093319443502715" + }, + "name": "Storage Account Queue Services", + "description": "This module deploys a Storage Account Queue Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "queues": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Queues to create." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "name": "default" + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "queueServices": { + "type": "Microsoft.Storage/storageAccounts/queueServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "properties": { + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]" + } + }, + "queueServices_diagnosticSettings": { + "copy": { + "name": "queueServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/queueServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "queueServices" + ] + }, + "queueServices_queues": { + "copy": { + "name": "queueServices_queues", + "count": "[length(coalesce(parameters('queues'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Queue-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "name": { + "value": "[coalesce(parameters('queues'), createArray())[copyIndex()].name]" + }, + "metadata": { + "value": "[tryGet(coalesce(parameters('queues'), createArray())[copyIndex()], 'metadata')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('queues'), createArray())[copyIndex()], 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "17963799770990303971" + }, + "name": "Storage Account Queues", + "description": "This module deploys a Storage Account Queue." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage queue to deploy." + } + }, + "metadata": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Storage/storageAccounts/queueServices/queues@2024-01-01#properties/properties/properties/metadata" + }, + "description": "Optional. A name-value pair that represents queue metadata." + }, + "defaultValue": {} + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Queue Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')]", + "Storage Queue Data Message Processor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8a0f0c08-91a1-4084-bc3d-661d67233fed')]", + "Storage Queue Data Message Sender": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c6a89b2d-59bc-44d0-9896-0f6e12d7b80a')]", + "Storage Queue Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '19e7f393-937e-4f77-808e-94535e297925')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::queueServices": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/queueServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]" + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "queue": { + "type": "Microsoft.Storage/storageAccounts/queueServices/queues", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "properties": { + "metadata": "[parameters('metadata')]" + } + }, + "queue_roleAssignments": { + "copy": { + "name": "queue_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/queueServices/{1}/queues/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', parameters('storageAccountName'), 'default', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "queue" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed queue." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed queue." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', parameters('storageAccountName'), 'default', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed queue." + }, + "value": "[resourceGroup().name]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share service." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/queueServices', parameters('storageAccountName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share service." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "storageAccount_tableServices": { + "condition": "[not(empty(parameters('tableServices')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Storage-TableServices', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('name')]" + }, + "diagnosticSettings": { + "value": "[tryGet(parameters('tableServices'), 'diagnosticSettings')]" + }, + "tables": { + "value": "[tryGet(parameters('tableServices'), 'tables')]" + }, + "corsRules": { + "value": "[tryGet(parameters('tableServices'), 'corsRules')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "13069389074590786512" + }, + "name": "Storage Account Table Services", + "description": "This module deploys a Storage Account Table Service." + }, + "definitions": { + "corsRuleType": { + "type": "object", + "properties": { + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of headers allowed to be part of the cross-origin request." + } + }, + "allowedMethods": { + "type": "array", + "allowedValues": [ + "CONNECT", + "DELETE", + "GET", + "HEAD", + "MERGE", + "OPTIONS", + "PATCH", + "POST", + "PUT", + "TRACE" + ], + "metadata": { + "description": "Required. A list of HTTP methods that are allowed to be executed by the origin." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of origin domains that will be allowed via CORS, or \"*\" to allow all domains." + } + }, + "exposedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of response headers to expose to CORS clients." + } + }, + "maxAgeInSeconds": { + "type": "int", + "metadata": { + "description": "Required. The number of seconds that the client/browser should cache a preflight response." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a cors rule." + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "tables": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. tables to create." + } + }, + "corsRules": { + "type": "array", + "items": { + "$ref": "#/definitions/corsRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The List of CORS rules. You can include up to five CorsRule elements in the request." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "name": "default" + }, + "resources": { + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "tableServices": { + "type": "Microsoft.Storage/storageAccounts/tableServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), variables('name'))]", + "properties": { + "cors": "[if(not(equals(parameters('corsRules'), null())), createObject('corsRules', parameters('corsRules')), null())]" + } + }, + "tableServices_diagnosticSettings": { + "copy": { + "name": "tableServices_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/tableServices/{1}', parameters('storageAccountName'), variables('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', variables('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "tableServices" + ] + }, + "tableServices_tables": { + "copy": { + "name": "tableServices_tables", + "count": "[length(parameters('tables'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-Table-{1}', deployment().name, copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('tables')[copyIndex()].name]" + }, + "storageAccountName": { + "value": "[parameters('storageAccountName')]" + }, + "roleAssignments": { + "value": "[tryGet(parameters('tables')[copyIndex()], 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10905926757212375091" + }, + "name": "Storage Account Table", + "description": "This module deploys a Storage Account Table." + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "maxLength": 24, + "metadata": { + "description": "Conditional. The name of the parent Storage Account. Required if the template is used in a standalone deployment." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the table." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Reader and Data Access": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c12c1c16-33a1-487b-954d-41c89c60f349')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "Storage Account Backup Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'e5e2a7ff-d759-4cd2-bb51-3152d37e2eb1')]", + "Storage Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '17d1049b-9a84-46fb-8f53-869881c3d3ab')]", + "Storage Account Key Operator Service Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]", + "Storage Table Data Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]", + "Storage Table Data Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '76199698-9eea-4c19-bc75-cec21354c6b6')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "storageAccount::tableServices": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts/tableServices", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}', parameters('storageAccountName'), 'default')]" + }, + "storageAccount": { + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2024-01-01", + "name": "[parameters('storageAccountName')]" + }, + "table": { + "type": "Microsoft.Storage/storageAccounts/tableServices/tables", + "apiVersion": "2024-01-01", + "name": "[format('{0}/{1}/{2}', parameters('storageAccountName'), 'default', parameters('name'))]" + }, + "table_roleAssignments": { + "copy": { + "name": "table_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}/tableServices/{1}/tables/{2}', parameters('storageAccountName'), 'default', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Storage/storageAccounts/tableServices/tables', parameters('storageAccountName'), 'default', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "table" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed file share service." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed file share service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/tableServices/tables', parameters('storageAccountName'), 'default', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed file share service." + }, + "value": "[resourceGroup().name]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed table service." + }, + "value": "[variables('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed table service." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts/tableServices', parameters('storageAccountName'), variables('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed table service." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + }, + "secretsExport": { + "condition": "[not(equals(parameters('secretsExportConfiguration'), null()))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-secrets-kv', uniqueString(deployment().name, parameters('location')))]", + "subscriptionId": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[2]]", + "resourceGroup": "[split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "keyVaultName": { + "value": "[last(split(tryGet(parameters('secretsExportConfiguration'), 'keyVaultResourceId'), '/'))]" + }, + "secretsToSet": { + "value": "[union(createArray(), if(contains(parameters('secretsExportConfiguration'), 'accessKey1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey1Name'), 'value', listKeys('storageAccount', '2024-01-01').keys[0].value)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'connectionString1Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'connectionString1Name'), 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2024-01-01').keys[0].value, environment().suffixes.storage))), createArray()), if(contains(parameters('secretsExportConfiguration'), 'accessKey2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'accessKey2Name'), 'value', listKeys('storageAccount', '2024-01-01').keys[1].value)), createArray()), if(contains(parameters('secretsExportConfiguration'), 'connectionString2Name'), createArray(createObject('name', tryGet(parameters('secretsExportConfiguration'), 'connectionString2Name'), 'value', format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2024-01-01').keys[1].value, environment().suffixes.storage))), createArray()))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9368972709899985618" + } + }, + "definitions": { + "secretSetOutputType": { + "type": "object", + "properties": { + "secretResourceId": { + "type": "string", + "metadata": { + "description": "The resourceId of the exported secret." + } + }, + "secretUri": { + "type": "string", + "metadata": { + "description": "The secret URI of the exported secret." + } + }, + "secretUriWithVersion": { + "type": "string", + "metadata": { + "description": "The secret URI with version of the exported secret." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the output of the secret set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "secretToSetType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the secret to set." + } + }, + "value": { + "type": "securestring", + "metadata": { + "description": "Required. The value of the secret to set." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for the secret to set via the secrets export feature.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Key Vault to set the ecrets in." + } + }, + "secretsToSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretToSetType" + }, + "metadata": { + "description": "Required. The secrets to set in the Key Vault." + } + } + }, + "resources": { + "keyVault": { + "existing": true, + "type": "Microsoft.KeyVault/vaults", + "apiVersion": "2024-11-01", + "name": "[parameters('keyVaultName')]" + }, + "secrets": { + "copy": { + "name": "secrets", + "count": "[length(parameters('secretsToSet'))]" + }, + "type": "Microsoft.KeyVault/vaults/secrets", + "apiVersion": "2024-11-01", + "name": "[format('{0}/{1}', parameters('keyVaultName'), parameters('secretsToSet')[copyIndex()].name)]", + "properties": { + "value": "[parameters('secretsToSet')[copyIndex()].value]" + } + } + }, + "outputs": { + "secretsSet": { + "type": "array", + "items": { + "$ref": "#/definitions/secretSetOutputType" + }, + "metadata": { + "description": "The references to the secrets exported to the provided Key Vault." + }, + "copy": { + "count": "[length(range(0, length(coalesce(parameters('secretsToSet'), createArray()))))]", + "input": { + "secretResourceId": "[resourceId('Microsoft.KeyVault/vaults/secrets', parameters('keyVaultName'), parameters('secretsToSet')[range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()]].name)]", + "secretUri": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUri]", + "secretUriWithVersion": "[reference(format('secrets[{0}]', range(0, length(coalesce(parameters('secretsToSet'), createArray())))[copyIndex()])).secretUriWithVersion]" + } + } + } + } + } + }, + "dependsOn": [ + "storageAccount" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the deployed storage account." + }, + "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the deployed storage account." + }, + "value": "[parameters('name')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group of the deployed storage account." + }, + "value": "[resourceGroup().name]" + }, + "primaryBlobEndpoint": { + "type": "string", + "metadata": { + "description": "The primary blob endpoint reference if blob services are deployed." + }, + "value": "[if(and(not(empty(parameters('blobServices'))), contains(parameters('blobServices'), 'containers')), reference(format('Microsoft.Storage/storageAccounts/{0}', parameters('name')), '2019-04-01').primaryEndpoints.blob, '')]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('storageAccount', '2024-01-01', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('storageAccount', '2024-01-01', 'full').location]" + }, + "serviceEndpoints": { + "type": "object", + "metadata": { + "description": "All service endpoints of the deployed storage account, Note Standard_LRS and Standard_ZRS accounts only have a blob service endpoint." + }, + "value": "[reference('storageAccount').primaryEndpoints]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the Storage Account." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('storageAccount_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + }, + "exportedSecrets": { + "$ref": "#/definitions/secretsOutputType", + "metadata": { + "description": "A hashtable of references to the secrets exported to the provided Key Vault. The key of each reference is each secret's name." + }, + "value": "[if(not(equals(parameters('secretsExportConfiguration'), null())), toObject(reference('secretsExport').outputs.secretsSet.value, lambda('secret', last(split(lambdaVariables('secret').secretResourceId, '/'))), lambda('secret', lambdaVariables('secret'))), createObject())]" + }, + "primaryAccessKey": { + "type": "securestring", + "metadata": { + "description": "The primary access key of the storage account." + }, + "value": "[listKeys('storageAccount', '2024-01-01').keys[0].value]" + }, + "secondaryAccessKey": { + "type": "securestring", + "metadata": { + "description": "The secondary access key of the storage account." + }, + "value": "[listKeys('storageAccount', '2024-01-01').keys[1].value]" + }, + "primaryConnectionString": { + "type": "securestring", + "metadata": { + "description": "The primary connection string of the storage account." + }, + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2024-01-01').keys[0].value, environment().suffixes.storage)]" + }, + "secondaryConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary connection string of the storage account." + }, + "value": "[format('DefaultEndpointsProtocol=https;AccountName={0};AccountKey={1};EndpointSuffix={2}', parameters('name'), listKeys('storageAccount', '2024-01-01').keys[1].value, environment().suffixes.storage)]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Storage Account." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('storageAccount').outputs.name.value, variables('existingName'))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Storage Account." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('storageAccount').outputs.resourceId.value, extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingSubscriptionId'), variables('existingResourceGroupName')), 'Microsoft.Storage/storageAccounts', variables('existingName')))]" + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID of the Storage Account." + }, + "value": "[if(empty(parameters('existingResourceId')), subscription().subscriptionId, variables('existingSubscriptionId'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource Group Name of the Storage Account." + }, + "value": "[if(empty(parameters('existingResourceId')), resourceGroup().name, variables('existingResourceGroupName'))]" + } + } + } + }, + "dependsOn": [ + "aiSearch", + "foundryAccount" + ] + }, + "cosmosDb": { + "condition": "[parameters('includeAssociatedResources')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.cosmosDb.{0}', variables('resourcesName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "existingResourceId": { + "value": "[tryGet(parameters('cosmosDbConfiguration'), 'existingResourceId')]" + }, + "name": { + "value": "[take(if(not(empty(tryGet(parameters('cosmosDbConfiguration'), 'name'))), parameters('cosmosDbConfiguration').name, format('cos{0}', variables('resourcesName'))), 44)]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "privateEndpointSubnetResourceId": { + "value": "[parameters('privateEndpointSubnetResourceId')]" + }, + "privateDnsZoneResourceId": { + "value": "[tryGet(parameters('cosmosDbConfiguration'), 'privateDnsZoneResourceId')]" + }, + "roleAssignments": { + "value": "[tryGet(parameters('cosmosDbConfiguration'), 'roleAssignments')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "12826689517691444097" + } + }, + "definitions": { + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "functions": [ + { + "namespace": "__bicep", + "members": { + "getResourceGroupName": { + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(greater(length(parameters('parts')), 4), parameters('parts')[4], resourceGroup().name)]" + }, + "metadata": { + "description": "Extracts the Resource Group Name from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getResourceName": { + "parameters": [ + { + "type": "string", + "nullable": true, + "name": "resourceId" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(and(and(not(empty(parameters('resourceId'))), contains(parameters('resourceId'), '/')), not(empty(parameters('parts')))), last(parameters('parts')), coalesce(parameters('resourceId'), ''))]" + }, + "metadata": { + "description": "Extracts the Resource Name from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getResourceParts": { + "parameters": [ + { + "type": "string", + "nullable": true, + "name": "resourceId" + } + ], + "output": { + "type": "array", + "items": { + "type": "string" + }, + "value": "[split(coalesce(parameters('resourceId'), ''), '/')]" + }, + "metadata": { + "description": "Splits Resource ID into its components.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + }, + "getSubscriptionId": { + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "name": "parts" + } + ], + "output": { + "type": "string", + "value": "[if(greater(length(parameters('parts')), 2), parameters('parts')[2], subscription().subscriptionId)]" + }, + "metadata": { + "description": "Extracts the Subscription ID from a Resource ID.", + "__bicep_imported_from!": { + "sourceTemplate": "parseResourceIdFunctions.bicep" + } + } + } + } + } + ], + "parameters": { + "name": { + "type": "string", + "maxLength": 44, + "metadata": { + "description": "Required. The name of the Cosmos DB." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Required. The location for the Cosmos DB." + } + }, + "existingResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full resource ID of an existing Cosmos DB to use instead of creating a new one." + } + }, + "privateEndpointSubnetResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource Id of an existing subnet to use for private connectivity. This is required along with 'privateDnsZoneResourceId' to establish private endpoints." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the private DNS zone for the Cosmos DB to establish private endpoints." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the role assignments for the Cosmos DB." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Resources/resourceGroups@2025-04-01#properties/tags" + }, + "description": "Optional. Specifies the resource tags for all the resources." + }, + "defaultValue": {} + } + }, + "variables": { + "existingResourceParts": "[__bicep.getResourceParts(parameters('existingResourceId'))]", + "existingName": "[__bicep.getResourceName(parameters('existingResourceId'), variables('existingResourceParts'))]", + "existingSubscriptionId": "[__bicep.getSubscriptionId(variables('existingResourceParts'))]", + "existingResourceGroupName": "[__bicep.getResourceGroupName(variables('existingResourceParts'))]", + "privateNetworkingEnabled": "[and(not(empty(parameters('privateDnsZoneResourceId'))), not(empty(parameters('privateEndpointSubnetResourceId'))))]" + }, + "resources": { + "existingCosmosDb": { + "condition": "[not(empty(parameters('existingResourceId')))]", + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", + "subscriptionId": "[variables('existingSubscriptionId')]", + "resourceGroup": "[variables('existingResourceGroupName')]", + "name": "[variables('existingName')]" + }, + "cosmosDb": { + "condition": "[empty(parameters('existingResourceId'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('avm.res.document-db.database-account.{0}', parameters('name')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "enableTelemetry": { + "value": "[parameters('enableTelemetry')]" + }, + "automaticFailover": { + "value": true + }, + "disableKeyBasedMetadataWriteAccess": { + "value": true + }, + "disableLocalAuthentication": { + "value": true + }, + "location": { + "value": "[parameters('location')]" + }, + "minimumTlsVersion": { + "value": "Tls12" + }, + "defaultConsistencyLevel": { + "value": "Session" + }, + "networkRestrictions": { + "value": { + "networkAclBypass": "AzureServices", + "publicNetworkAccess": "[if(variables('privateNetworkingEnabled'), 'Disabled', 'Enabled')]" + } + }, + "privateEndpoints": "[if(variables('privateNetworkingEnabled'), createObject('value', createArray(createObject('privateDnsZoneGroup', createObject('privateDnsZoneGroupConfigs', createArray(createObject('privateDnsZoneResourceId', parameters('privateDnsZoneResourceId')))), 'service', 'Sql', 'subnetResourceId', parameters('privateEndpointSubnetResourceId')))), createObject('value', createArray()))]", + "roleAssignments": { + "value": "[parameters('roleAssignments')]" + }, + "tags": { + "value": "[parameters('tags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "17715929342484596741" + }, + "name": "Azure Cosmos DB account", + "description": "This module deploys an Azure Cosmos DB account. The API used for the account is determined by the child resources that are deployed." + }, + "definitions": { + "privateEndpointOutputType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + } + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + } + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group ID for the private endpoint group." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "fully-qualified domain name (FQDN) that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "A list of private IP addresses for the private endpoint." + } + } + } + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + } + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The IDs of the network interfaces associated with the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the private endpoint output." + } + }, + "failoverLocationType": { + "type": "object", + "properties": { + "failoverPriority": { + "type": "int", + "metadata": { + "description": "Required. The failover priority of the region. A failover priority of 0 indicates a write region. The maximum value for a failover priority = (total number of regions - 1). Failover priority values must be unique for each of the regions in which the database account exists." + } + }, + "isZoneRedundant": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Flag to indicate whether or not this region is an AvailabilityZone region. Defaults to true." + } + }, + "locationName": { + "type": "string", + "metadata": { + "description": "Required. The name of the region." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the failover location." + } + }, + "dataPlaneRoleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique name of the role assignment." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier of the Azure Cosmos DB for NoSQL native role-based access control definition." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated Microsoft Entra ID principal to which access is being granted through this role-based access control assignment. The tenant ID for the principal is inferred using the tenant associated with the subscription." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB for NoSQL native role-based access control assignment." + } + }, + "dataPlaneRoleDefinitionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique identifier of the role-based access control definition." + } + }, + "roleName": { + "type": "string", + "metadata": { + "description": "Required. A user-friendly name for the role-based access control definition. This must be unique within the database account." + } + }, + "dataActions": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of data actions that are allowed." + } + }, + "assignableScopes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A set of fully-qualified scopes at or below which role-based access control assignments may be created using this definition. This setting allows application of this definition on the entire account or any underlying resource. This setting must have at least one element. Scopes higher than the account level are not enforceable as assignable scopes. Resources referenced in assignable scopes do not need to exist at creation. Defaults to the current account scope." + } + }, + "assignments": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of role-based access control assignments to be created for the definition." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB for NoSQL or Table native role-based access control definition." + } + }, + "sqlDatabaseType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the database ." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request units per second. Will be ignored if `autoscaleSettingsMaxThroughput` is used. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level. Defaults to 400." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the autoscale settings and represents maximum throughput the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If the value is not set, then autoscale will be disabled. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "containers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the container." + } + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "maxLength": 3, + "metadata": { + "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." + } + }, + "analyticalStorageTtl": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "maxValue": 1000000, + "metadata": { + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level." + } + }, + "conflictResolutionPolicy": { + "type": "object", + "properties": { + "conflictResolutionPath": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The conflict resolution path in the case of LastWriterWins mode. Required if `mode` is set to 'LastWriterWins'." + } + }, + "conflictResolutionProcedure": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The procedure to resolve conflicts in the case of custom mode. Required if `mode` is set to 'Custom'." + } + }, + "mode": { + "type": "string", + "allowedValues": [ + "Custom", + "LastWriterWins" + ], + "metadata": { + "description": "Required. Indicates the conflict resolution mode." + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." + } + }, + "defaultTtl": { + "type": "int", + "nullable": true, + "minValue": -1, + "maxValue": 2147483647, + "metadata": { + "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." + } + }, + "indexingPolicy": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Indexing policy of the container." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "Hash", + "MultiHash" + ], + "nullable": true, + "metadata": { + "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." + } + }, + "version": { + "type": "int", + "allowedValues": [ + 1, + 2 + ], + "nullable": true, + "metadata": { + "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used." + } + }, + "uniqueKeyPolicyKeys": { + "type": "array", + "items": { + "type": "object", + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. List of paths must be unique for each document in the Azure Cosmos DB service." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. Set of containers to deploy in the database." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an Azure Cosmos DB for NoSQL database." + } + }, + "networkRestrictionType": { + "type": "object", + "properties": { + "ipRules": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A single IPv4 address or a single IPv4 address range in Classless Inter-Domain Routing (CIDR) format. Provided IPs must be well-formatted and cannot be contained in one of the following ranges: `10.0.0.0/8`, `100.64.0.0/10`, `172.16.0.0/12`, `192.168.0.0/16`, since these are not enforceable by the IP address filter. Example of valid inputs: `23.40.210.245` or `23.40.210.0/8`." + } + }, + "networkAclBypass": { + "type": "string", + "allowedValues": [ + "AzureServices", + "None" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specifies the network ACL bypass for Azure services. Default to \"None\"." + } + }, + "publicNetworkAccess": { + "type": "string", + "allowedValues": [ + "Disabled", + "Enabled" + ], + "nullable": true, + "metadata": { + "description": "Optional. Whether requests from the public network are allowed. Default to \"Disabled\"." + } + }, + "virtualNetworkRules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of a subnet." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. List of virtual network access control list (ACL) rules configured for the account." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the network restriction." + } + }, + "_1.privateEndpointCustomDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointIpConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "_1.privateEndpointPrivateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS Zone Group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + } + }, + "metadata": { + "description": "Required. The private DNS Zone Groups to associate the Private Endpoint. A DNS Zone Group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "diagnosticSettingFullType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the diagnostic setting." + } + }, + "logCategoriesAndGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category for a resource type this setting is applied to. Set the specific logs to collect here." + } + }, + "categoryGroup": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a Diagnostic Log category group for a resource type this setting is applied to. Set to `allLogs` to collect all logs." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of logs that will be streamed. \"allLogs\" includes all possible logs for the resource. Set to `[]` to disable log collection." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if both logs & metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "privateEndpointMultiServiceType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private endpoint." + } + }, + "location": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The location to deploy the private endpoint to." + } + }, + "privateLinkServiceConnectionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private link connection to create." + } + }, + "service": { + "type": "string", + "metadata": { + "description": "Required. The subresource to deploy the private endpoint for. For example \"blob\", \"table\", \"queue\" or \"file\" for a Storage Account's Private Endpoints." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "resourceGroupResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The resource ID of the Resource Group the Private Endpoint will be created in. If not specified, the Resource Group of the provided Virtual Network Subnet is used." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/_1.privateEndpointPrivateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "isManualConnection": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. If Manual Private Link Connection is required." + } + }, + "manualConnectionRequestMessage": { + "type": "string", + "nullable": true, + "maxLength": 140, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with the manual connection request." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointCustomDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/_1.privateEndpointIpConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Network/privateEndpoints@2024-07-01#properties/tags" + }, + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "enableTelemetry": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a private endpoint. To be used if the private endpoint's default service / groupId can NOT be assumed (i.e., for services that have more than one subresource, like Storage Account with Blob (blob, table, queue, file, ...).", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.1" + } + } + }, + "sqlRoleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + } + }, + "metadata": { + "description": "The type for the SQL Role Assignments.", + "__bicep_imported_from!": { + "sourceTemplate": "sql-role-definition/main.bicep" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the account." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Defaults to the current resource group scope location. Location for all resources." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.DocumentDB/databaseAccounts@2024-11-15#properties/tags" + }, + "description": "Optional. Tags for the resource." + }, + "nullable": true + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "databaseAccountOfferType": { + "type": "string", + "defaultValue": "Standard", + "allowedValues": [ + "Standard" + ], + "metadata": { + "description": "Optional. The offer type for the account. Defaults to \"Standard\"." + } + }, + "failoverLocations": { + "type": "array", + "items": { + "$ref": "#/definitions/failoverLocationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The set of locations enabled for the account. Defaults to the location where the account is deployed." + } + }, + "zoneRedundant": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates whether the single-region account is zone redundant. Defaults to true. This property is ignored for multi-region accounts." + } + }, + "defaultConsistencyLevel": { + "type": "string", + "defaultValue": "Session", + "allowedValues": [ + "Eventual", + "ConsistentPrefix", + "Session", + "BoundedStaleness", + "Strong" + ], + "metadata": { + "description": "Optional. The default consistency level of the account. Defaults to \"Session\"." + } + }, + "disableLocalAuthentication": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Opt-out of local authentication and ensure that only Microsoft Entra can be used exclusively for authentication. Defaults to true." + } + }, + "enableAnalyticalStorage": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Flag to indicate whether to enable storage analytics. Defaults to false." + } + }, + "automaticFailover": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable automatic failover for regions. Defaults to true." + } + }, + "enableFreeTier": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Flag to indicate whether \"Free Tier\" is enabled. Defaults to false." + } + }, + "enableMultipleWriteLocations": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Enables the account to write in multiple locations. Periodic backup must be used if enabled. Defaults to false." + } + }, + "disableKeyBasedMetadataWriteAccess": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Disable write operations on metadata resources (databases, containers, throughput) via account keys. Defaults to true." + } + }, + "maxStalenessPrefix": { + "type": "int", + "defaultValue": 100000, + "minValue": 1, + "maxValue": 2147483647, + "metadata": { + "description": "Optional. The maximum stale requests. Required for \"BoundedStaleness\" consistency level. Valid ranges, Single Region: 10 to 1000000. Multi Region: 100000 to 1000000. Defaults to 100000." + } + }, + "maxIntervalInSeconds": { + "type": "int", + "defaultValue": 300, + "minValue": 5, + "maxValue": 86400, + "metadata": { + "description": "Optional. The maximum lag time in minutes. Required for \"BoundedStaleness\" consistency level. Valid ranges, Single Region: 5 to 84600. Multi Region: 300 to 86400. Defaults to 300." + } + }, + "serverVersion": { + "type": "string", + "defaultValue": "4.2", + "allowedValues": [ + "3.2", + "3.6", + "4.0", + "4.2", + "5.0", + "6.0", + "7.0" + ], + "metadata": { + "description": "Optional. Specifies the MongoDB server version to use if using Azure Cosmos DB for MongoDB RU. Defaults to \"4.2\"." + } + }, + "sqlDatabases": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlDatabaseType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration for databases when using Azure Cosmos DB for NoSQL." + } + }, + "mongodbDatabases": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Configuration for databases when using Azure Cosmos DB for MongoDB RU." + } + }, + "gremlinDatabases": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Configuration for databases when using Azure Cosmos DB for Apache Gremlin." + } + }, + "tables": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Configuration for databases when using Azure Cosmos DB for Table." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "totalThroughputLimit": { + "type": "int", + "defaultValue": -1, + "metadata": { + "description": "Optional. The total throughput limit imposed on this account in request units per second (RU/s). Default to unlimited throughput." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of control plane Azure role-based access control assignments." + } + }, + "dataPlaneRoleDefinitions": { + "type": "array", + "items": { + "$ref": "#/definitions/dataPlaneRoleDefinitionType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configurations for Azure Cosmos DB for NoSQL native role-based access control definitions. Allows the creations of custom role definitions." + } + }, + "dataPlaneRoleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/dataPlaneRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configurations for Azure Cosmos DB for NoSQL native role-based access control assignments." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingFullType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings for the service." + } + }, + "capabilitiesToAdd": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "allowedValues": [ + "EnableCassandra", + "EnableTable", + "EnableGremlin", + "EnableMongo", + "DisableRateLimitingResponses", + "EnableServerless", + "EnableNoSQLVectorSearch", + "EnableNoSQLFullTextSearch", + "EnableMaterializedViews", + "DeleteAllItemsByPartitionKey" + ], + "metadata": { + "description": "Optional. A list of Azure Cosmos DB specific capabilities for the account." + } + }, + "backupPolicyType": { + "type": "string", + "defaultValue": "Continuous", + "allowedValues": [ + "Periodic", + "Continuous" + ], + "metadata": { + "description": "Optional. Configures the backup mode. Periodic backup must be used if multiple write locations are used. Defaults to \"Continuous\"." + } + }, + "backupPolicyContinuousTier": { + "type": "string", + "defaultValue": "Continuous30Days", + "allowedValues": [ + "Continuous30Days", + "Continuous7Days" + ], + "metadata": { + "description": "Optional. Configuration values to specify the retention period for continuous mode backup. Default to \"Continuous30Days\"." + } + }, + "backupIntervalInMinutes": { + "type": "int", + "defaultValue": 240, + "minValue": 60, + "maxValue": 1440, + "metadata": { + "description": "Optional. An integer representing the interval in minutes between two backups. This setting only applies to the periodic backup type. Defaults to 240." + } + }, + "backupRetentionIntervalInHours": { + "type": "int", + "defaultValue": 8, + "minValue": 2, + "maxValue": 720, + "metadata": { + "description": "Optional. An integer representing the time (in hours) that each backup is retained. This setting only applies to the periodic backup type. Defaults to 8." + } + }, + "backupStorageRedundancy": { + "type": "string", + "defaultValue": "Local", + "allowedValues": [ + "Geo", + "Local", + "Zone" + ], + "metadata": { + "description": "Optional. Setting that indicates the type of backup residency. This setting only applies to the periodic backup type. Defaults to \"Local\"." + } + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointMultiServiceType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Configuration details for private endpoints. For security reasons, it is advised to use private endpoints whenever possible." + } + }, + "networkRestrictions": { + "$ref": "#/definitions/networkRestrictionType", + "defaultValue": { + "ipRules": [], + "virtualNetworkRules": [], + "publicNetworkAccess": "Disabled" + }, + "metadata": { + "description": "Optional. The network configuration of this module. Defaults to `{ ipRules: [], virtualNetworkRules: [], publicNetworkAccess: 'Disabled' }`." + } + }, + "minimumTlsVersion": { + "type": "string", + "defaultValue": "Tls12", + "allowedValues": [ + "Tls12" + ], + "metadata": { + "description": "Optional. Setting that indicates the minimum allowed TLS version. Azure Cosmos DB for MongoDB RU and Apache Cassandra only work with TLS 1.2 or later. Defaults to \"Tls12\" (TLS 1.2)." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInControlPlaneRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "enableReferencedModulesTelemetry": false, + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', null())), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInControlPlaneRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Cosmos DB Account Reader Role": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fbdf93bf-df7d-467e-a4d2-9458aa1360c8')]", + "Cosmos DB Operator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa')]", + "CosmosBackupOperator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'db7b14f2-5adf-42da-9f96-f2ee17bab5cb')]", + "CosmosRestoreOperator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5432c526-bc82-444a-b7ba-57c5b0b5b34f')]", + "DocumentDB Account Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5bd9cd88-fe45-4216-938b-f97437e15450')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-07-01", + "name": "[format('46d3xbcp.res.documentdb-databaseaccount.{0}.{1}', replace('0.16.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "identity": "[variables('identity')]", + "kind": "[if(not(empty(parameters('mongodbDatabases'))), 'MongoDB', 'GlobalDocumentDB')]", + "properties": "[shallowMerge(createArray(createObject('databaseAccountOfferType', parameters('databaseAccountOfferType'), 'backupPolicy', shallowMerge(createArray(createObject('type', parameters('backupPolicyType')), if(equals(parameters('backupPolicyType'), 'Continuous'), createObject('continuousModeProperties', createObject('tier', parameters('backupPolicyContinuousTier'))), createObject()), if(equals(parameters('backupPolicyType'), 'Periodic'), createObject('periodicModeProperties', createObject('backupIntervalInMinutes', parameters('backupIntervalInMinutes'), 'backupRetentionIntervalInHours', parameters('backupRetentionIntervalInHours'), 'backupStorageRedundancy', parameters('backupStorageRedundancy'))), createObject()))), 'capabilities', map(coalesce(parameters('capabilitiesToAdd'), createArray()), lambda('capability', createObject('name', lambdaVariables('capability')))), 'minimalTlsVersion', parameters('minimumTlsVersion'), 'capacity', createObject('totalThroughputLimit', parameters('totalThroughputLimit')), 'publicNetworkAccess', coalesce(tryGet(parameters('networkRestrictions'), 'publicNetworkAccess'), 'Disabled')), if(or(or(or(not(empty(parameters('sqlDatabases'))), not(empty(parameters('mongodbDatabases')))), not(empty(parameters('gremlinDatabases')))), not(empty(parameters('tables')))), createObject('consistencyPolicy', shallowMerge(createArray(createObject('defaultConsistencyLevel', parameters('defaultConsistencyLevel')), if(equals(parameters('defaultConsistencyLevel'), 'BoundedStaleness'), createObject('maxStalenessPrefix', parameters('maxStalenessPrefix'), 'maxIntervalInSeconds', parameters('maxIntervalInSeconds')), createObject()))), 'enableMultipleWriteLocations', parameters('enableMultipleWriteLocations'), 'locations', if(not(empty(parameters('failoverLocations'))), map(parameters('failoverLocations'), lambda('failoverLocation', createObject('failoverPriority', lambdaVariables('failoverLocation').failoverPriority, 'locationName', lambdaVariables('failoverLocation').locationName, 'isZoneRedundant', coalesce(tryGet(lambdaVariables('failoverLocation'), 'isZoneRedundant'), true())))), createArray(createObject('failoverPriority', 0, 'locationName', parameters('location'), 'isZoneRedundant', parameters('zoneRedundant')))), 'ipRules', map(coalesce(tryGet(parameters('networkRestrictions'), 'ipRules'), createArray()), lambda('ipRule', createObject('ipAddressOrRange', lambdaVariables('ipRule')))), 'virtualNetworkRules', map(coalesce(tryGet(parameters('networkRestrictions'), 'virtualNetworkRules'), createArray()), lambda('rule', createObject('id', lambdaVariables('rule').subnetResourceId, 'ignoreMissingVNetServiceEndpoint', false()))), 'networkAclBypass', coalesce(tryGet(parameters('networkRestrictions'), 'networkAclBypass'), 'None'), 'isVirtualNetworkFilterEnabled', or(not(empty(tryGet(parameters('networkRestrictions'), 'ipRules'))), not(empty(tryGet(parameters('networkRestrictions'), 'virtualNetworkRules')))), 'enableFreeTier', parameters('enableFreeTier'), 'enableAutomaticFailover', parameters('automaticFailover'), 'enableAnalyticalStorage', parameters('enableAnalyticalStorage')), createObject()), if(or(not(empty(parameters('mongodbDatabases'))), not(empty(parameters('gremlinDatabases')))), createObject('disableLocalAuth', false(), 'disableKeyBasedMetadataWriteAccess', false()), createObject('disableLocalAuth', parameters('disableLocalAuthentication'), 'disableKeyBasedMetadataWriteAccess', parameters('disableKeyBasedMetadataWriteAccess'))), if(not(empty(parameters('mongodbDatabases'))), createObject('apiProperties', createObject('serverVersion', parameters('serverVersion'))), createObject())))]" + }, + "databaseAccount_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_diagnosticSettings": { + "copy": { + "name": "databaseAccount_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + }, + { + "name": "logs", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs'))))]", + "input": { + "categoryGroup": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'categoryGroup')]", + "category": "[tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'category')]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logCategoriesAndGroups'), createArray(createObject('categoryGroup', 'allLogs')))[copyIndex('logs')], 'enabled'), true())]" + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_roleAssignments": { + "copy": { + "name": "databaseAccount_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_sqlDatabases": { + "copy": { + "name": "databaseAccount_sqlDatabases", + "count": "[length(coalesce(parameters('sqlDatabases'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqldb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('sqlDatabases'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(parameters('sqlDatabases'), createArray())[copyIndex()].name]" + }, + "containers": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'containers')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'throughput')]" + }, + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(coalesce(parameters('sqlDatabases'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "7141543733238879531" + }, + "name": "DocumentDB Database Account SQL Databases", + "description": "This module deploys a SQL Database in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the SQL database ." + } + }, + "containers": { + "type": "array", + "items": { + "type": "object" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of containers to deploy in the SQL database." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. Setting throughput at the database level is only recommended for development/test or when workload across all containers in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the SQL database resource." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]" + }, + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', if(equals(parameters('autoscaleSettingsMaxThroughput'), null()), parameters('throughput'), null()), 'autoscaleSettings', if(not(equals(parameters('autoscaleSettingsMaxThroughput'), null())), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null())))]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "container": { + "copy": { + "name": "container", + "count": "[length(coalesce(parameters('containers'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqldb-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('containers'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "sqlDatabaseName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('containers'), createArray())[copyIndex()].name]" + }, + "analyticalStorageTtl": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'analyticalStorageTtl')]" + }, + "autoscaleSettingsMaxThroughput": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'autoscaleSettingsMaxThroughput')]" + }, + "conflictResolutionPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'conflictResolutionPolicy')]" + }, + "defaultTtl": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'defaultTtl')]" + }, + "indexingPolicy": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'indexingPolicy')]" + }, + "kind": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'kind')]" + }, + "version": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'version')]" + }, + "paths": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'paths')]" + }, + "throughput": "[if(and(or(not(equals(parameters('throughput'), null())), not(equals(parameters('autoscaleSettingsMaxThroughput'), null()))), equals(tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'throughput'), null())), createObject('value', -1), createObject('value', tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'throughput')))]", + "uniqueKeyPolicyKeys": { + "value": "[tryGet(coalesce(parameters('containers'), createArray())[copyIndex()], 'uniqueKeyPolicyKeys')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "1789954443166349986" + }, + "name": "DocumentDB Database Account SQL Database Containers", + "description": "This module deploys a SQL Database Container in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "sqlDatabaseName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent SQL Database. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the container." + } + }, + "analyticalStorageTtl": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Default to 0. Indicates how long data should be retained in the analytical store, for a container. Analytical store is enabled when ATTL is set with a value other than 0. If the value is set to -1, the analytical store retains all historical data, irrespective of the retention of the data in the transactional store." + } + }, + "conflictResolutionPolicy": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. The conflict resolution policy for the container. Conflicts and conflict resolution policies are applicable if the Azure Cosmos DB account is configured with multiple write regions." + } + }, + "defaultTtl": { + "type": "int", + "defaultValue": -1, + "minValue": -1, + "maxValue": 2147483647, + "metadata": { + "description": "Optional. Default to -1. Default time to live (in seconds). With Time to Live or TTL, Azure Cosmos DB provides the ability to delete items automatically from a container after a certain time period. If the value is set to \"-1\", it is equal to infinity, and items don't expire by default." + } + }, + "throughput": { + "type": "int", + "defaultValue": 400, + "metadata": { + "description": "Optional. Default to 400. Request Units per second. Will be ignored if autoscaleSettingsMaxThroughput is used. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "autoscaleSettingsMaxThroughput": { + "type": "int", + "nullable": true, + "maxValue": 1000000, + "metadata": { + "description": "Optional. Specifies the Autoscale settings and represents maximum throughput, the resource can scale up to. The autoscale throughput should have valid throughput values between 1000 and 1000000 inclusive in increments of 1000. If value is set to null, then autoscale will be disabled. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the container level and not at the database level." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the SQL Database resource." + } + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "minLength": 1, + "maxLength": 3, + "metadata": { + "description": "Required. List of paths using which data within the container can be partitioned. For kind=MultiHash it can be up to 3. For anything else it needs to be exactly 1." + } + }, + "indexingPolicy": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Indexing policy of the container." + } + }, + "uniqueKeyPolicyKeys": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. The unique key policy configuration containing a list of unique keys that enforces uniqueness constraint on documents in the collection in the Azure Cosmos DB service." + } + }, + "kind": { + "type": "string", + "defaultValue": "Hash", + "allowedValues": [ + "Hash", + "MultiHash" + ], + "metadata": { + "description": "Optional. Default to Hash. Indicates the kind of algorithm used for partitioning." + } + }, + "version": { + "type": "int", + "defaultValue": 1, + "allowedValues": [ + 1, + 2 + ], + "metadata": { + "description": "Optional. Default to 1 for Hash and 2 for MultiHash - 1 is not allowed for MultiHash. Version of the partition key definition." + } + } + }, + "variables": { + "copy": [ + { + "name": "partitionKeyPaths", + "count": "[length(parameters('paths'))]", + "input": "[if(startsWith(parameters('paths')[copyIndex('partitionKeyPaths')], '/'), parameters('paths')[copyIndex('partitionKeyPaths')], format('/{0}', parameters('paths')[copyIndex('partitionKeyPaths')]))]" + } + ], + "containerResourceParams": "[union(createObject('conflictResolutionPolicy', parameters('conflictResolutionPolicy'), 'defaultTtl', parameters('defaultTtl'), 'id', parameters('name'), 'indexingPolicy', if(not(empty(parameters('indexingPolicy'))), parameters('indexingPolicy'), null()), 'partitionKey', createObject('paths', variables('partitionKeyPaths'), 'kind', parameters('kind'), 'version', if(equals(parameters('kind'), 'MultiHash'), 2, parameters('version'))), 'uniqueKeyPolicy', if(not(empty(parameters('uniqueKeyPolicyKeys'))), createObject('uniqueKeys', parameters('uniqueKeyPolicyKeys')), null())), if(not(equals(parameters('analyticalStorageTtl'), 0)), createObject('analyticalStorageTtl', parameters('analyticalStorageTtl')), createObject()))]" + }, + "resources": { + "databaseAccount::sqlDatabase": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('sqlDatabaseName'))]" + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "container": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('sqlDatabaseName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": "[variables('containerResourceParams')]", + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', if(and(equals(parameters('autoscaleSettingsMaxThroughput'), null()), not(equals(parameters('throughput'), -1))), parameters('throughput'), null()), 'autoscaleSettings', if(not(equals(parameters('autoscaleSettingsMaxThroughput'), null())), createObject('maxThroughput', parameters('autoscaleSettingsMaxThroughput')), null())))]" + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the container." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the container." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('databaseAccountName'), parameters('sqlDatabaseName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the container was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "sqlDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL database." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_sqlRoleDefinitions": { + "copy": { + "name": "databaseAccount_sqlRoleDefinitions", + "count": "[length(coalesce(parameters('dataPlaneRoleDefinitions'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqlrd-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()], 'name')]" + }, + "dataActions": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()], 'dataActions')]" + }, + "roleName": { + "value": "[coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()].roleName]" + }, + "assignableScopes": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()], 'assignableScopes')]" + }, + "sqlRoleAssignments": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleDefinitions'), createArray())[copyIndex()], 'assignments')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9570871897890815068" + }, + "name": "DocumentDB Database Account SQL Role Definitions.", + "description": "This module deploys a SQL Role Definision in a CosmosDB Account." + }, + "definitions": { + "sqlRoleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the SQL Role Assignments." + } + } + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The unique identifier of the Role Definition." + } + }, + "roleName": { + "type": "string", + "metadata": { + "description": "Required. A user-friendly name for the Role Definition. Must be unique for the database account." + } + }, + "dataActions": { + "type": "array", + "items": { + "type": "string" + }, + "defaultValue": [], + "metadata": { + "description": "Optional. An array of data actions that are allowed." + } + }, + "assignableScopes": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. A set of fully qualified Scopes at or below which Role Assignments may be created using this Role Definition. This will allow application of this Role Definition on the entire database account or any underlying Database / Collection. Must have at least one element. Scopes higher than Database account are not enforceable as assignable Scopes. Note that resources referenced in assignable Scopes need not exist. Defaults to the current account." + } + }, + "sqlRoleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/sqlRoleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. An array of SQL Role Assignments to be created for the SQL Role Definition." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "enableReferencedModulesTelemetry": false + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroledefinition.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleDefinition": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), 'sql-role')))]", + "properties": { + "assignableScopes": "[coalesce(parameters('assignableScopes'), createArray(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]", + "permissions": [ + { + "dataActions": "[parameters('dataActions')]" + } + ], + "roleName": "[parameters('roleName')]", + "type": "CustomRole" + } + }, + "databaseAccount_sqlRoleAssignments": { + "copy": { + "name": "databaseAccount_sqlRoleAssignments", + "count": "[length(coalesce(parameters('sqlRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqlra-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "roleDefinitionId": { + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), 'sql-role')))]" + }, + "principalId": { + "value": "[coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('sqlRoleAssignments'), createArray())[copyIndex()], 'name')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10102303164433641479" + }, + "name": "DocumentDB Database Account SQL Role Assignments.", + "description": "This module deploys a SQL Role Assignment in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier of the associated SQL Role Definition." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroleassignment.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Assignment." + }, + "value": "[coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL Role Assignment." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "sqlRoleDefinition" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Definition." + }, + "value": "[coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), 'sql-role'))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL Role Definition." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('databaseAccountName'), coalesce(parameters('name'), guid(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), parameters('databaseAccountName'), 'sql-role')))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + }, + "roleName": { + "type": "string", + "metadata": { + "description": "The role name of the SQL Role Definition." + }, + "value": "[reference('sqlRoleDefinition').roleName]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_sqlRoleAssignments": { + "copy": { + "name": "databaseAccount_sqlRoleAssignments", + "count": "[length(coalesce(parameters('dataPlaneRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-sqlra-{1}', uniqueString(deployment().name), copyIndex())]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "roleDefinitionId": { + "value": "[coalesce(parameters('dataPlaneRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]" + }, + "principalId": { + "value": "[coalesce(parameters('dataPlaneRoleAssignments'), createArray())[copyIndex()].principalId]" + }, + "name": { + "value": "[tryGet(coalesce(parameters('dataPlaneRoleAssignments'), createArray())[copyIndex()], 'name')]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "10102303164433641479" + }, + "name": "DocumentDB Database Account SQL Role Assignments.", + "description": "This module deploys a SQL Role Assignment in a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name unique identifier of the SQL Role Assignment." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier for the associated AAD principal in the AAD graph to which access is being granted through this Role Assignment. Tenant ID for the principal is inferred using the tenant associated with the subscription." + } + }, + "roleDefinitionId": { + "type": "string", + "metadata": { + "description": "Required. The unique identifier of the associated SQL Role Definition." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.doctdb-dbacct-sqlroleassignment.{0}.{1}', replace('-..--..-', '.', '-'), substring(uniqueString(deployment().name), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "sqlRoleAssignment": { + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]", + "properties": { + "principalId": "[parameters('principalId')]", + "roleDefinitionId": "[parameters('roleDefinitionId')]", + "scope": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the SQL Role Assignment." + }, + "value": "[coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName'))))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the SQL Role Assignment." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments', parameters('databaseAccountName'), coalesce(parameters('name'), guid(parameters('roleDefinitionId'), parameters('principalId'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')))))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the SQL Role Definition was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_mongodbDatabases": { + "copy": { + "name": "databaseAccount_mongodbDatabases", + "count": "[length(coalesce(parameters('mongodbDatabases'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-mongodb-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "collections": { + "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'collections')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('mongodbDatabases'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "9160691107424630312" + }, + "name": "DocumentDB Database Account MongoDB Databases", + "description": "This module deploys a MongoDB Database within a CosmosDB Account." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Cosmos DB database account. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the mongodb database." + } + }, + "throughput": { + "type": "int", + "defaultValue": 400, + "metadata": { + "description": "Optional. Request Units per second. Setting throughput at the database level is only recommended for development/test or when workload across all collections in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." + } + }, + "collections": { + "type": "array", + "nullable": true, + "metadata": { + "description": "Optional. Collections in the mongodb database." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the resource." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "mongodbDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]" + }, + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', parameters('throughput')))]" + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "mongodbDatabase_collections": { + "copy": { + "name": "mongodbDatabase_collections", + "count": "[length(coalesce(parameters('collections'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-collection-{1}', uniqueString(deployment().name, parameters('name')), coalesce(parameters('collections'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "mongodbDatabaseName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].name]" + }, + "indexes": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].indexes]" + }, + "shardKey": { + "value": "[coalesce(parameters('collections'), createArray())[copyIndex()].shardKey]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('collections'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "14050805189442830205" + }, + "name": "DocumentDB Database Account MongoDB Database Collections", + "description": "This module deploys a MongoDB Database Collection." + }, + "parameters": { + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Cosmos DB database account. Required if the template is used in a standalone deployment." + } + }, + "mongodbDatabaseName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent mongodb database. Required if the template is used in a standalone deployment." + } + }, + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the collection." + } + }, + "throughput": { + "type": "int", + "defaultValue": 400, + "metadata": { + "description": "Optional. Request Units per second. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the collection level and not at the database level." + } + }, + "indexes": { + "type": "array", + "metadata": { + "description": "Required. Indexes for the collection." + } + }, + "shardKey": { + "type": "object", + "metadata": { + "description": "Required. ShardKey for the collection." + } + } + }, + "resources": [ + { + "type": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('mongodbDatabaseName'), parameters('name'))]", + "properties": { + "options": "[if(contains(reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('databaseAccountName')), '2024-11-15').capabilities, createObject('name', 'EnableServerless')), null(), createObject('throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]", + "indexes": "[parameters('indexes')]", + "shardKey": "[parameters('shardKey')]" + } + } + } + ], + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the mongodb database collection." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the mongodb database collection." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections', parameters('databaseAccountName'), parameters('mongodbDatabaseName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the mongodb database collection was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "mongodbDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the mongodb database." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the mongodb database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/mongodbDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the mongodb database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_gremlinDatabases": { + "copy": { + "name": "databaseAccount_gremlinDatabases", + "count": "[length(coalesce(parameters('gremlinDatabases'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-gremlin-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "graphs": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'graphs')]" + }, + "maxThroughput": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'maxThroughput')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('gremlinDatabases'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "16834580070429190924" + }, + "name": "DocumentDB Database Account Gremlin Databases", + "description": "This module deploys a Gremlin Database within a CosmosDB Account." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Gremlin database." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the Gremlin database resource." + } + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Gremlin database. Required if the template is used in a standalone deployment." + } + }, + "graphs": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. Array of graphs to deploy in the Gremlin database." + } + }, + "maxThroughput": { + "type": "int", + "defaultValue": 4000, + "metadata": { + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`. Setting throughput at the database level is only recommended for development/test or when workload across all graphs in the shared throughput database is uniform. For best performance for large production workloads, it is recommended to set dedicated throughput (autoscale or manual) at the graph level and not at the database level." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "gremlinDatabase": { + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('maxThroughput')), null()), 'throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]" + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "gremlinDatabase_gremlinGraphs": { + "copy": { + "name": "gremlinDatabase_gremlinGraphs", + "count": "[length(parameters('graphs'))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-gremlindb-{1}', uniqueString(deployment().name, parameters('name')), parameters('graphs')[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('graphs')[copyIndex()].name]" + }, + "gremlinDatabaseName": { + "value": "[parameters('name')]" + }, + "databaseAccountName": { + "value": "[parameters('databaseAccountName')]" + }, + "indexingPolicy": { + "value": "[tryGet(parameters('graphs')[copyIndex()], 'indexingPolicy')]" + }, + "partitionKeyPaths": "[if(not(empty(parameters('graphs')[copyIndex()].partitionKeyPaths)), createObject('value', parameters('graphs')[copyIndex()].partitionKeyPaths), createObject('value', createArray()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "15062578211366932944" + }, + "name": "DocumentDB Database Accounts Gremlin Databases Graphs", + "description": "This module deploys a DocumentDB Database Accounts Gremlin Database Graph." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the graph." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags of the Gremlin graph resource." + } + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Database Account. Required if the template is used in a standalone deployment." + } + }, + "gremlinDatabaseName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Gremlin Database. Required if the template is used in a standalone deployment." + } + }, + "indexingPolicy": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Optional. Indexing policy of the graph." + } + }, + "partitionKeyPaths": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Optional. List of paths using which data within the container can be partitioned." + } + } + }, + "resources": { + "databaseAccount::gremlinDatabase": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('gremlinDatabaseName'))]" + }, + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "gremlinGraph": { + "type": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}/{2}', parameters('databaseAccountName'), parameters('gremlinDatabaseName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "resource": { + "id": "[parameters('name')]", + "indexingPolicy": "[if(not(empty(parameters('indexingPolicy'))), parameters('indexingPolicy'), null())]", + "partitionKey": { + "paths": "[if(not(empty(parameters('partitionKeyPaths'))), parameters('partitionKeyPaths'), null())]" + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the graph." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the graph." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs', parameters('databaseAccountName'), parameters('gremlinDatabaseName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the graph was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "gremlinDatabase" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the Gremlin database." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Gremlin database." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/gremlinDatabases', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Gremlin database was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_tables": { + "copy": { + "name": "databaseAccount_tables", + "count": "[length(coalesce(parameters('tables'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-table-{1}', uniqueString(deployment().name, parameters('location')), coalesce(parameters('tables'), createArray())[copyIndex()].name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "databaseAccountName": { + "value": "[parameters('name')]" + }, + "name": { + "value": "[coalesce(parameters('tables'), createArray())[copyIndex()].name]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "maxThroughput": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'maxThroughput')]" + }, + "throughput": { + "value": "[tryGet(coalesce(parameters('tables'), createArray())[copyIndex()], 'throughput')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.37.4.10188", + "templateHash": "3429971823201332257" + }, + "name": "Azure Cosmos DB account tables", + "description": "This module deploys a table within an Azure Cosmos DB Account." + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the table." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags for the table." + } + }, + "databaseAccountName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Azure Cosmos DB account. Required if the template is used in a standalone deployment." + } + }, + "maxThroughput": { + "type": "int", + "defaultValue": 4000, + "metadata": { + "description": "Optional. Represents maximum throughput, the resource can scale up to. Cannot be set together with `throughput`. If `throughput` is set to something else than -1, this autoscale setting is ignored." + } + }, + "throughput": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Request Units per second (for example 10000). Cannot be set together with `maxThroughput`." + } + } + }, + "resources": { + "databaseAccount": { + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2024-11-15", + "name": "[parameters('databaseAccountName')]" + }, + "table": { + "type": "Microsoft.DocumentDB/databaseAccounts/tables", + "apiVersion": "2024-11-15", + "name": "[format('{0}/{1}', parameters('databaseAccountName'), parameters('name'))]", + "tags": "[parameters('tags')]", + "properties": { + "options": "[if(contains(reference('databaseAccount').capabilities, createObject('name', 'EnableServerless')), createObject(), createObject('autoscaleSettings', if(equals(parameters('throughput'), null()), createObject('maxThroughput', parameters('maxThroughput')), null()), 'throughput', parameters('throughput')))]", + "resource": { + "id": "[parameters('name')]" + } + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the table." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the table." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts/tables', parameters('databaseAccountName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the table was created in." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + }, + "databaseAccount_privateEndpoints": { + "copy": { + "name": "databaseAccount_privateEndpoints", + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]" + }, + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-dbAccount-PrivateEndpoint-{1}', uniqueString(deployment().name, parameters('location')), copyIndex())]", + "subscriptionId": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[2]]", + "resourceGroup": "[split(coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'resourceGroupResourceId'), resourceGroup().id), '/')[4]]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'name'), format('pep-{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex()))]" + }, + "privateLinkServiceConnections": "[if(not(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true())), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service))))), createObject('value', null()))]", + "manualPrivateLinkServiceConnections": "[if(equals(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'isManualConnection'), true()), createObject('value', createArray(createObject('name', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateLinkServiceConnectionName'), format('{0}-{1}-{2}', last(split(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), '/')), coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service, copyIndex())), 'properties', createObject('privateLinkServiceId', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name')), 'groupIds', createArray(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].service), 'requestMessage', coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'manualConnectionRequestMessage'), 'Manual approval required.'))))), createObject('value', null()))]", + "subnetResourceId": { + "value": "[coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId]" + }, + "enableTelemetry": { + "value": "[variables('enableReferencedModulesTelemetry')]" + }, + "location": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'location'), reference(split(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()].subnetResourceId, '/subnets/')[0], '2020-06-01', 'Full').location)]" + }, + "lock": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'lock'), parameters('lock'))]" + }, + "privateDnsZoneGroup": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'privateDnsZoneGroup')]" + }, + "roleAssignments": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'roleAssignments')]" + }, + "tags": { + "value": "[coalesce(tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'tags'), parameters('tags'))]" + }, + "customDnsConfigs": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customDnsConfigs')]" + }, + "ipConfigurations": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'ipConfigurations')]" + }, + "applicationSecurityGroupResourceIds": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'applicationSecurityGroupResourceIds')]" + }, + "customNetworkInterfaceName": { + "value": "[tryGet(coalesce(parameters('privateEndpoints'), createArray())[copyIndex()], 'customNetworkInterfaceName')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "12389807800450456797" + }, + "name": "Private Endpoints", + "description": "This module deploys a Private Endpoint." + }, + "definitions": { + "privateDnsZoneGroupType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Private DNS Zone Group." + } + }, + "privateDnsZoneGroupConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "metadata": { + "description": "Required. The private DNS zone groups to associate the private endpoint. A DNS zone group can support up to 5 DNS zones." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "ipConfigurationType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the resource that is unique within a resource group." + } + }, + "properties": { + "type": "object", + "properties": { + "groupId": { + "type": "string", + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "memberName": { + "type": "string", + "metadata": { + "description": "Required. The member name of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string." + } + }, + "privateIPAddress": { + "type": "string", + "metadata": { + "description": "Required. A private IP address obtained from the private endpoint's subnet." + } + } + }, + "metadata": { + "description": "Required. Properties of private endpoint IP configurations." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "privateLinkServiceConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the private link service connection." + } + }, + "properties": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. The ID of a group obtained from the remote resource that this private endpoint should connect to. If used with private link service connection, this property must be defined as empty string array `[]`." + } + }, + "privateLinkServiceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of private link service." + } + }, + "requestMessage": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. A message passed to the owner of the remote resource with this connection request. Restricted to 140 chars." + } + } + }, + "metadata": { + "description": "Required. Properties of private link service connection." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "customDnsConfigType": { + "type": "object", + "properties": { + "fqdn": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. FQDN that resolves to private endpoint IP address." + } + }, + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "Required. A list of private IP addresses of the private endpoint." + } + } + }, + "metadata": { + "__bicep_export!": true + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + }, + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_imported_from!": { + "sourceTemplate": "private-dns-zone-group/main.bicep" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.5.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the private endpoint resource to create." + } + }, + "subnetResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of the subnet where the endpoint needs to be created." + } + }, + "applicationSecurityGroupResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Application security groups in which the private endpoint IP configuration is included." + } + }, + "customNetworkInterfaceName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The custom name of the network interface attached to the private endpoint." + } + }, + "ipConfigurations": { + "type": "array", + "items": { + "$ref": "#/definitions/ipConfigurationType" + }, + "nullable": true, + "metadata": { + "description": "Optional. A list of IP configurations of the private endpoint. This will be used to map to the First Party Service endpoints." + } + }, + "privateDnsZoneGroup": { + "$ref": "#/definitions/privateDnsZoneGroupType", + "nullable": true, + "metadata": { + "description": "Optional. The private DNS zone group to configure for the private endpoint." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "tags": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. Tags to be applied on all resources/resource groups in this deployment." + } + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Custom DNS configurations." + } + }, + "manualPrivateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Used when the network admin does not have access to approve connections to the remote resource. Required if `privateLinkServiceConnections` is empty." + } + }, + "privateLinkServiceConnections": { + "type": "array", + "items": { + "$ref": "#/definitions/privateLinkServiceConnectionType" + }, + "nullable": true, + "metadata": { + "description": "Conditional. A grouping of information about the connection to the remote resource. Required if `manualPrivateLinkServiceConnections` is empty." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "builtInRoleNames": { + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "DNS Resolver Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0f2ebee7-ffd4-4fc0-b3b7-664099fdad5d')]", + "DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'befefa01-2a29-4197-83a8-272ff33ce314')]", + "Domain Services Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'eeaeda52-9324-47f6-8069-5d5bade478b2')]", + "Domain Services Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '361898ef-9ed1-48c2-849c-a832951106bb')]", + "Network Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4d97b98b-1d4f-4787-a291-c67834d212e7')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Private DNS Zone Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b12aa53e-6015-4669-85d0-8515ebb3ae7f')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.network-privateendpoint.{0}.{1}', replace('0.11.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "privateEndpoint": { + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "copy": [ + { + "name": "applicationSecurityGroups", + "count": "[length(coalesce(parameters('applicationSecurityGroupResourceIds'), createArray()))]", + "input": { + "id": "[coalesce(parameters('applicationSecurityGroupResourceIds'), createArray())[copyIndex('applicationSecurityGroups')]]" + } + } + ], + "customDnsConfigs": "[coalesce(parameters('customDnsConfigs'), createArray())]", + "customNetworkInterfaceName": "[coalesce(parameters('customNetworkInterfaceName'), '')]", + "ipConfigurations": "[coalesce(parameters('ipConfigurations'), createArray())]", + "manualPrivateLinkServiceConnections": "[coalesce(parameters('manualPrivateLinkServiceConnections'), createArray())]", + "privateLinkServiceConnections": "[coalesce(parameters('privateLinkServiceConnections'), createArray())]", + "subnet": { + "id": "[parameters('subnetResourceId')]" + } + } + }, + "privateEndpoint_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_roleAssignments": { + "copy": { + "name": "privateEndpoint_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.Network/privateEndpoints', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "privateEndpoint" + ] + }, + "privateEndpoint_privateDnsZoneGroup": { + "condition": "[not(empty(parameters('privateDnsZoneGroup')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2022-09-01", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[tryGet(parameters('privateDnsZoneGroup'), 'name')]" + }, + "privateEndpointName": { + "value": "[parameters('name')]" + }, + "privateDnsZoneConfigs": { + "value": "[parameters('privateDnsZoneGroup').privateDnsZoneGroupConfigs]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.34.44.8038", + "templateHash": "13997305779829540948" + }, + "name": "Private Endpoint Private DNS Zone Groups", + "description": "This module deploys a Private Endpoint Private DNS Zone Group." + }, + "definitions": { + "privateDnsZoneGroupConfigType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the private DNS zone group config." + } + }, + "privateDnsZoneResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource id of the private DNS zone." + } + } + }, + "metadata": { + "__bicep_export!": true + } + } + }, + "parameters": { + "privateEndpointName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent private endpoint. Required if the template is used in a standalone deployment." + } + }, + "privateDnsZoneConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/privateDnsZoneGroupConfigType" + }, + "minLength": 1, + "maxLength": 5, + "metadata": { + "description": "Required. Array of private DNS zone configurations of the private DNS zone group. A DNS zone group can support up to 5 DNS zones." + } + }, + "name": { + "type": "string", + "defaultValue": "default", + "metadata": { + "description": "Optional. The name of the private DNS zone group." + } + } + }, + "variables": { + "copy": [ + { + "name": "privateDnsZoneConfigsVar", + "count": "[length(parameters('privateDnsZoneConfigs'))]", + "input": { + "name": "[coalesce(tryGet(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')], 'name'), last(split(parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId, '/')))]", + "properties": { + "privateDnsZoneId": "[parameters('privateDnsZoneConfigs')[copyIndex('privateDnsZoneConfigsVar')].privateDnsZoneResourceId]" + } + } + } + ] + }, + "resources": { + "privateEndpoint": { + "existing": true, + "type": "Microsoft.Network/privateEndpoints", + "apiVersion": "2024-05-01", + "name": "[parameters('privateEndpointName')]" + }, + "privateDnsZoneGroup": { + "type": "Microsoft.Network/privateEndpoints/privateDnsZoneGroups", + "apiVersion": "2024-05-01", + "name": "[format('{0}/{1}', parameters('privateEndpointName'), parameters('name'))]", + "properties": { + "privateDnsZoneConfigs": "[variables('privateDnsZoneConfigsVar')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint DNS zone group." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint DNS zone group." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints/privateDnsZoneGroups', parameters('privateEndpointName'), parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint DNS zone group was deployed into." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "privateEndpoint" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group the private endpoint was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the private endpoint." + }, + "value": "[resourceId('Microsoft.Network/privateEndpoints', parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the private endpoint." + }, + "value": "[parameters('name')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('privateEndpoint', '2024-05-01', 'full').location]" + }, + "customDnsConfigs": { + "type": "array", + "items": { + "$ref": "#/definitions/customDnsConfigType" + }, + "metadata": { + "description": "The custom DNS configurations of the private endpoint." + }, + "value": "[reference('privateEndpoint').customDnsConfigs]" + }, + "networkInterfaceResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "metadata": { + "description": "The resource IDs of the network interfaces associated with the private endpoint." + }, + "value": "[map(reference('privateEndpoint').networkInterfaces, lambda('nic', lambdaVariables('nic').id))]" + }, + "groupId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The group Id for the private endpoint Group." + }, + "value": "[coalesce(tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'manualPrivateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0), tryGet(tryGet(tryGet(tryGet(reference('privateEndpoint'), 'privateLinkServiceConnections'), 0, 'properties'), 'groupIds'), 0))]" + } + } + } + }, + "dependsOn": [ + "databaseAccount" + ] + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the database account." + }, + "value": "[parameters('name')]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the database account." + }, + "value": "[resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('name'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the database account was created in." + }, + "value": "[resourceGroup().name]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('databaseAccount', '2024-11-15', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('databaseAccount', '2024-11-15', 'full').location]" + }, + "endpoint": { + "type": "string", + "metadata": { + "description": "The endpoint of the database account." + }, + "value": "[reference('databaseAccount').documentEndpoint]" + }, + "privateEndpoints": { + "type": "array", + "items": { + "$ref": "#/definitions/privateEndpointOutputType" + }, + "metadata": { + "description": "The private endpoints of the database account." + }, + "copy": { + "count": "[length(coalesce(parameters('privateEndpoints'), createArray()))]", + "input": { + "name": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.name.value]", + "resourceId": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.resourceId.value]", + "groupId": "[tryGet(tryGet(reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs, 'groupId'), 'value')]", + "customDnsConfigs": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.customDnsConfigs.value]", + "networkInterfaceResourceIds": "[reference(format('databaseAccount_privateEndpoints[{0}]', copyIndex())).outputs.networkInterfaceResourceIds.value]" + } + } + }, + "primaryReadWriteKey": { + "type": "securestring", + "metadata": { + "description": "The primary read-write key." + }, + "value": "[listKeys('databaseAccount', '2024-11-15').primaryMasterKey]" + }, + "primaryReadOnlyKey": { + "type": "securestring", + "metadata": { + "description": "The primary read-only key." + }, + "value": "[listKeys('databaseAccount', '2024-11-15').primaryReadonlyMasterKey]" + }, + "primaryReadWriteConnectionString": { + "type": "securestring", + "metadata": { + "description": "The primary read-write connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2024-11-15').connectionStrings[0].connectionString]" + }, + "primaryReadOnlyConnectionString": { + "type": "securestring", + "metadata": { + "description": "The primary read-only connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2024-11-15').connectionStrings[2].connectionString]" + }, + "secondaryReadWriteKey": { + "type": "securestring", + "metadata": { + "description": "The secondary read-write key." + }, + "value": "[listKeys('databaseAccount', '2024-11-15').secondaryMasterKey]" + }, + "secondaryReadOnlyKey": { + "type": "securestring", + "metadata": { + "description": "The secondary read-only key." + }, + "value": "[listKeys('databaseAccount', '2024-11-15').secondaryReadonlyMasterKey]" + }, + "secondaryReadWriteConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary read-write connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2024-11-15').connectionStrings[1].connectionString]" + }, + "secondaryReadOnlyConnectionString": { + "type": "securestring", + "metadata": { + "description": "The secondary read-only connection string." + }, + "value": "[listConnectionStrings('databaseAccount', '2024-11-15').connectionStrings[3].connectionString]" + } + } + } + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the Cosmos DB." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('cosmosDb').outputs.name.value, variables('existingName'))]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Cosmos DB." + }, + "value": "[if(empty(parameters('existingResourceId')), reference('cosmosDb').outputs.resourceId.value, extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingSubscriptionId'), variables('existingResourceGroupName')), 'Microsoft.DocumentDB/databaseAccounts', variables('existingName')))]" + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Subscription ID of the Cosmos DB." + }, + "value": "[if(empty(parameters('existingResourceId')), subscription().subscriptionId, variables('existingSubscriptionId'))]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Resource Group Name of the Cosmos DB." + }, + "value": "[if(empty(parameters('existingResourceId')), resourceGroup().name, variables('existingResourceGroupName'))]" + } + } + } + } + }, + "foundryProject": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.project.main.{0}', variables('projectName')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('projectName')]" + }, + "desc": "[if(not(empty(tryGet(tryGet(parameters('aiFoundryConfiguration'), 'project'), 'desc'))), createObject('value', parameters('aiFoundryConfiguration').project.desc), createObject('value', 'This is the default project for AI Foundry.'))]", + "displayName": "[if(not(empty(tryGet(tryGet(parameters('aiFoundryConfiguration'), 'project'), 'displayName'))), createObject('value', parameters('aiFoundryConfiguration').project.displayName), createObject('value', format('{0} Default Project', parameters('baseName'))))]", + "accountName": { + "value": "[reference('foundryAccount').outputs.name.value]" + }, + "location": { + "value": "[reference('foundryAccount').outputs.location.value]" + }, + "createAccountCapabilityHost": { + "value": "[and(variables('createCapabilityHosts'), empty(tryGet(tryGet(parameters('aiFoundryConfiguration'), 'networking'), 'agentServiceSubnetResourceId')))]" + }, + "createProjectCapabilityHost": { + "value": "[variables('createCapabilityHosts')]" + }, + "storageAccountConnection": "[if(parameters('includeAssociatedResources'), createObject('value', createObject('resourceName', reference('storageAccount').outputs.name.value, 'subscriptionId', reference('storageAccount').outputs.subscriptionId.value, 'resourceGroupName', reference('storageAccount').outputs.resourceGroupName.value)), createObject('value', null()))]", + "aiSearchConnection": "[if(parameters('includeAssociatedResources'), createObject('value', createObject('resourceName', reference('aiSearch').outputs.name.value, 'subscriptionId', reference('aiSearch').outputs.subscriptionId.value, 'resourceGroupName', reference('aiSearch').outputs.resourceGroupName.value)), createObject('value', null()))]", + "cosmosDbConnection": "[if(parameters('includeAssociatedResources'), createObject('value', createObject('resourceName', reference('cosmosDb').outputs.name.value, 'subscriptionId', reference('cosmosDb').outputs.subscriptionId.value, 'resourceGroupName', reference('cosmosDb').outputs.resourceGroupName.value)), createObject('value', null()))]", + "tags": { + "value": "[parameters('tags')]" + }, + "lock": { + "value": "[parameters('lock')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "8997226659817763257" + }, + "name": "AI Foundry Project", + "description": "Creates an AI Foundry project and any associated Azure service connections." + }, + "definitions": { + "azureConnectionType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the project connection. Will default to the resource name if not provided." + } + }, + "resourceName": { + "type": "string", + "metadata": { + "description": "Required. The resource name of the Azure resource for the connection." + } + }, + "subscriptionId": { + "type": "string", + "metadata": { + "description": "Required. The subscription ID of the resource." + } + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Required. The resource group name of the resource." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "Type representing values to create an Azure connection to an AI Foundry project." + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 64, + "metadata": { + "description": "Required. The name of the AI Foundry project." + } + }, + "displayName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The display name of the AI Foundry project." + } + }, + "desc": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the AI Foundry project." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Specifies the location for all the Azure resources." + } + }, + "accountName": { + "type": "string", + "metadata": { + "description": "Required. Name of the existing parent Foundry Account resource." + } + }, + "createAccountCapabilityHost": { + "type": "bool", + "metadata": { + "description": "Required. Whether to create the capability host for the Foundry account. Requires associated resource connections to be provided." + } + }, + "createProjectCapabilityHost": { + "type": "bool", + "metadata": { + "description": "Required. Whether to create the capability host for the Foundry project. Requires associated resource connections to be provided." + } + }, + "cosmosDbConnection": { + "$ref": "#/definitions/azureConnectionType", + "nullable": true, + "metadata": { + "description": "Optional. Azure Cosmos DB connection for the project." + } + }, + "aiSearchConnection": { + "$ref": "#/definitions/azureConnectionType", + "nullable": true, + "metadata": { + "description": "Optional. Azure Cognitive Search connection for the project." + } + }, + "storageAccountConnection": { + "$ref": "#/definitions/azureConnectionType", + "nullable": true, + "metadata": { + "description": "Optional. Storage Account connection for the project." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.Resources/resourceGroups@2025-04-01#properties/tags" + }, + "description": "Optional. Tags to be applied to the resources." + }, + "defaultValue": {} + } + }, + "variables": { + "hasConnection": "[or(or(not(empty(parameters('cosmosDbConnection'))), not(empty(parameters('aiSearchConnection')))), not(empty(parameters('storageAccountConnection'))))]", + "createProjectCapabilityHostInternal": "[and(and(and(parameters('createProjectCapabilityHost'), not(empty(parameters('cosmosDbConnection')))), not(empty(parameters('aiSearchConnection')))), not(empty(parameters('storageAccountConnection'))))]", + "createAccountCapabilityHostInternal": "[and(and(and(parameters('createAccountCapabilityHost'), not(empty(parameters('cosmosDbConnection')))), not(empty(parameters('aiSearchConnection')))), not(empty(parameters('storageAccountConnection'))))]" + }, + "resources": { + "foundryAccount": { + "existing": true, + "type": "Microsoft.CognitiveServices/accounts", + "apiVersion": "2025-06-01", + "name": "[parameters('accountName')]" + }, + "storageAccount": { + "condition": "[not(empty(parameters('storageAccountConnection')))]", + "existing": true, + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-01-01", + "subscriptionId": "[parameters('storageAccountConnection').subscriptionId]", + "resourceGroup": "[parameters('storageAccountConnection').resourceGroupName]", + "name": "[parameters('storageAccountConnection').resourceName]" + }, + "aiSearch": { + "condition": "[not(empty(parameters('aiSearchConnection')))]", + "existing": true, + "type": "Microsoft.Search/searchServices", + "apiVersion": "2025-05-01", + "subscriptionId": "[parameters('aiSearchConnection').subscriptionId]", + "resourceGroup": "[parameters('aiSearchConnection').resourceGroupName]", + "name": "[parameters('aiSearchConnection').resourceName]" + }, + "cosmosDb": { + "condition": "[not(empty(parameters('cosmosDbConnection')))]", + "existing": true, + "type": "Microsoft.DocumentDB/databaseAccounts", + "apiVersion": "2025-04-15", + "subscriptionId": "[parameters('cosmosDbConnection').subscriptionId]", + "resourceGroup": "[parameters('cosmosDbConnection').resourceGroupName]", + "name": "[parameters('cosmosDbConnection').resourceName]" + }, + "project": { + "type": "Microsoft.CognitiveServices/accounts/projects", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}', parameters('accountName'), parameters('name'))]", + "location": "[parameters('location')]", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "displayName": "[if(not(empty(parameters('displayName'))), parameters('displayName'), parameters('name'))]", + "description": "[if(not(empty(parameters('desc'))), parameters('desc'), parameters('name'))]" + }, + "tags": "[parameters('tags')]" + }, + "cosmosDbConnectionResource": { + "condition": "[not(empty(parameters('cosmosDbConnection')))]", + "type": "Microsoft.CognitiveServices/accounts/projects/connections", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('name'), parameters('cosmosDbConnection').resourceName)]", + "properties": { + "category": "CosmosDB", + "target": "[reference('cosmosDb').documentEndpoint]", + "authType": "AAD", + "metadata": { + "ApiType": "Azure", + "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('cosmosDbConnection').subscriptionId, parameters('cosmosDbConnection').resourceGroupName), 'Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbConnection').resourceName)]", + "location": "[reference('cosmosDb', '2025-04-15', 'full').location]" + } + }, + "dependsOn": [ + "cosmosDb", + "cosmosDbRoleAssignments", + "project", + "waitForProjectScript" + ] + }, + "storageAccountConnectionResource": { + "condition": "[not(empty(parameters('storageAccountConnection')))]", + "type": "Microsoft.CognitiveServices/accounts/projects/connections", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('name'), parameters('storageAccountConnection').resourceName)]", + "properties": { + "category": "AzureStorageAccount", + "target": "[reference('storageAccount').primaryEndpoints.blob]", + "authType": "AAD", + "metadata": { + "ApiType": "Azure", + "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('storageAccountConnection').subscriptionId, parameters('storageAccountConnection').resourceGroupName), 'Microsoft.Storage/storageAccounts', parameters('storageAccountConnection').resourceName)]", + "location": "[reference('storageAccount', '2025-01-01', 'full').location]" + } + }, + "dependsOn": [ + "cosmosDbConnectionResource", + "project", + "storageAccount", + "storageAccountRoleAssignments", + "waitForProjectScript" + ] + }, + "aiSearchConnectionResource": { + "condition": "[not(empty(parameters('aiSearchConnection')))]", + "type": "Microsoft.CognitiveServices/accounts/projects/connections", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('name'), parameters('aiSearchConnection').resourceName)]", + "properties": { + "category": "CognitiveSearch", + "target": "[format('https://{0}.search.windows.net/', parameters('aiSearchConnection').resourceName)]", + "authType": "AAD", + "metadata": { + "ApiType": "Azure", + "ResourceId": "[extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', parameters('aiSearchConnection').subscriptionId, parameters('aiSearchConnection').resourceGroupName), 'Microsoft.Search/searchServices', parameters('aiSearchConnection').resourceName)]", + "location": "[reference('aiSearch', '2025-05-01', 'full').location]" + } + }, + "dependsOn": [ + "aiSearch", + "aiSearchRoleAssignments", + "cosmosDbConnectionResource", + "project", + "storageAccountConnectionResource", + "waitForProjectScript" + ] + }, + "accountCapabilityHost": { + "condition": "[variables('createAccountCapabilityHostInternal')]", + "type": "Microsoft.CognitiveServices/accounts/capabilityHosts", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}', parameters('accountName'), format('chagent{0}', replace(parameters('accountName'), '-', '')))]", + "properties": { + "capabilityHostKind": "Agents", + "tags": "[parameters('tags')]" + }, + "dependsOn": [ + "aiSearchConnectionResource", + "cosmosDbConnectionResource", + "project", + "storageAccountConnectionResource", + "waitForConnectionsScript" + ] + }, + "capabilityHost": { + "condition": "[variables('createProjectCapabilityHostInternal')]", + "type": "Microsoft.CognitiveServices/accounts/projects/capabilityHosts", + "apiVersion": "2025-04-01-preview", + "name": "[format('{0}/{1}/{2}', parameters('accountName'), parameters('name'), format('chagent{0}', replace(parameters('name'), '-', '')))]", + "properties": { + "capabilityHostKind": "Agents", + "threadStorageConnections": [ + "[format('{0}', parameters('cosmosDbConnection').resourceName)]" + ], + "vectorStoreConnections": [ + "[format('{0}', parameters('aiSearchConnection').resourceName)]" + ], + "storageConnections": [ + "[format('{0}', parameters('storageAccountConnection').resourceName)]" + ], + "tags": "[parameters('tags')]" + }, + "dependsOn": [ + "accountCapabilityHost", + "aiSearchConnectionResource", + "cosmosDbConnectionResource", + "project", + "storageAccountConnectionResource", + "waitForConnectionsScript" + ] + }, + "projectLock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.CognitiveServices/accounts/{0}/projects/{1}', parameters('accountName'), parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "capabilityHost", + "project" + ] + }, + "waitForProjectScript": { + "condition": "[variables('hasConnection')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.project.waitDeploymentScript.waitForProject.{0}', parameters('name')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[format('script-wait-proj-{0}', parameters('name'))]" + }, + "location": { + "value": "[parameters('location')]" + }, + "seconds": { + "value": 30 + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "16818353602719638288" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the deployment script." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Required. Location for the deployment script." + } + }, + "seconds": { + "type": "int", + "metadata": { + "description": "Required. Sleep/wait time for the deployment script in seconds." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2023-08-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "kind": "AzurePowerShell", + "properties": { + "azPowerShellVersion": "11.0", + "scriptContent": "[format('Write-Host \"Waiting for {0} seconds...\" ; Start-Sleep -Seconds {1}; Write-Host \"Wait complete.\"', parameters('seconds'), parameters('seconds'))]", + "timeout": "P1D", + "cleanupPreference": "Always", + "retentionInterval": "P1D" + } + } + ] + } + }, + "dependsOn": [ + "project" + ] + }, + "cosmosDbRoleAssignments": { + "condition": "[not(empty(parameters('cosmosDbConnection')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.project.role-assign.cosmosDb.{0}', parameters('name')), 64)]", + "subscriptionId": "[parameters('cosmosDbConnection').subscriptionId]", + "resourceGroup": "[parameters('cosmosDbConnection').resourceGroupName]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "cosmosDbName": { + "value": "[parameters('cosmosDbConnection').resourceName]" + }, + "projectIdentityPrincipalId": { + "value": "[reference('project', '2025-04-01-preview', 'full').identity.principalId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "2297586848184477491" + } + }, + "parameters": { + "cosmosDbName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Cosmos DB account." + } + }, + "projectIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the project identity." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.DocumentDB/databaseAccounts/{0}', parameters('cosmosDbName'))]", + "name": "[guid(parameters('projectIdentityPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa'), resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName')))]", + "properties": { + "principalId": "[parameters('projectIdentityPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '230815da-be43-4aae-9cb4-875f7bd000aa')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "project", + "waitForProjectScript" + ] + }, + "storageAccountRoleAssignments": { + "condition": "[not(empty(parameters('storageAccountConnection')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.project.role-assign.storageAccount.{0}', parameters('name')), 64)]", + "subscriptionId": "[parameters('storageAccountConnection').subscriptionId]", + "resourceGroup": "[parameters('storageAccountConnection').resourceGroupName]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountConnection').resourceName]" + }, + "projectIdentityPrincipalId": { + "value": "[reference('project', '2025-04-01-preview', 'full').identity.principalId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "16405095293780360423" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage account." + } + }, + "projectIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the project identity." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'), parameters('storageAccountName'), parameters('projectIdentityPrincipalId'))]", + "properties": { + "principalId": "[parameters('projectIdentityPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "project", + "waitForProjectScript" + ] + }, + "aiSearchRoleAssignments": { + "condition": "[not(empty(parameters('aiSearchConnection')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.project.role-assign.aiSearch.{0}', parameters('name')), 64)]", + "subscriptionId": "[parameters('aiSearchConnection').subscriptionId]", + "resourceGroup": "[parameters('aiSearchConnection').resourceGroupName]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "aiSearchName": { + "value": "[parameters('aiSearchConnection').resourceName]" + }, + "projectIdentityPrincipalId": { + "value": "[reference('project', '2025-04-01-preview', 'full').identity.principalId]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "16025941000331400340" + } + }, + "parameters": { + "aiSearchName": { + "type": "string", + "metadata": { + "description": "Required. The name of the AI Search resource." + } + }, + "projectIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the project identity." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "name": "[guid(parameters('projectIdentityPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]", + "properties": { + "principalId": "[parameters('projectIdentityPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '8ebe5a00-799e-43f5-93ac-243d3dce84a7')]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Search/searchServices/{0}', parameters('aiSearchName'))]", + "name": "[guid(parameters('projectIdentityPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0'), resourceId('Microsoft.Search/searchServices', parameters('aiSearchName')))]", + "properties": { + "principalId": "[parameters('projectIdentityPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', '7ca78c08-252a-4471-8644-bb5ff32d4ba0')]", + "principalType": "ServicePrincipal" + } + } + ] + } + }, + "dependsOn": [ + "project", + "waitForProjectScript" + ] + }, + "waitForConnectionsScript": { + "condition": "[and(variables('hasConnection'), or(variables('createAccountCapabilityHostInternal'), variables('createProjectCapabilityHostInternal')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.project.waitDeploymentScript.waitForConn.{0}', parameters('name')), 64)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[format('script-wait-conns-{0}', parameters('name'))]" + }, + "location": { + "value": "[parameters('location')]" + }, + "seconds": { + "value": 60 + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "16818353602719638288" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the deployment script." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Required. Location for the deployment script." + } + }, + "seconds": { + "type": "int", + "metadata": { + "description": "Required. Sleep/wait time for the deployment script in seconds." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deploymentScripts", + "apiVersion": "2023-08-01", + "name": "[parameters('name')]", + "location": "[parameters('location')]", + "kind": "AzurePowerShell", + "properties": { + "azPowerShellVersion": "11.0", + "scriptContent": "[format('Write-Host \"Waiting for {0} seconds...\" ; Start-Sleep -Seconds {1}; Write-Host \"Wait complete.\"', parameters('seconds'), parameters('seconds'))]", + "timeout": "P1D", + "cleanupPreference": "Always", + "retentionInterval": "P1D" + } + } + ] + } + }, + "dependsOn": [ + "aiSearchConnectionResource", + "cosmosDbConnectionResource", + "project", + "storageAccountConnectionResource", + "waitForProjectScript" + ] + }, + "cosmosDbSqlRoleAssignments": { + "condition": "[and(not(empty(parameters('cosmosDbConnection'))), variables('createProjectCapabilityHostInternal'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.project.role-assign.cosmosDbDataPlane.{0}', parameters('name')), 64)]", + "subscriptionId": "[parameters('cosmosDbConnection').subscriptionId]", + "resourceGroup": "[parameters('cosmosDbConnection').resourceGroupName]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "cosmosDbName": { + "value": "[parameters('cosmosDbConnection').resourceName]" + }, + "projectIdentityPrincipalId": { + "value": "[reference('project', '2025-04-01-preview', 'full').identity.principalId]" + }, + "projectWorkspaceId": { + "value": "[format('{0}-{1}-{2}-{3}-{4}', if(greaterOrEquals(length(reference('project').internalId), 8), substring(reference('project').internalId, 0, 8), ''), if(greaterOrEquals(length(reference('project').internalId), 12), substring(reference('project').internalId, 8, 4), ''), if(greaterOrEquals(length(reference('project').internalId), 16), substring(reference('project').internalId, 12, 4), ''), if(greaterOrEquals(length(reference('project').internalId), 20), substring(reference('project').internalId, 16, 4), ''), if(greaterOrEquals(length(reference('project').internalId), 32), substring(reference('project').internalId, 20, 12), ''))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "11649050309693252000" + } + }, + "parameters": { + "cosmosDbName": { + "type": "string", + "metadata": { + "description": "Required. The name of the Cosmos DB account." + } + }, + "projectIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the project identity." + } + }, + "projectWorkspaceId": { + "type": "string", + "metadata": { + "description": "Required. The project workspace ID." + } + } + }, + "variables": { + "cosmosContainerNameSuffixes": [ + "thread-message-store", + "system-thread-message-store", + "agent-entity-store" + ], + "cosmosDefaultSqlRoleDefinitionId": "[resourceId('Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions', parameters('cosmosDbName'), '00000000-0000-0000-0000-000000000002')]" + }, + "resources": [ + { + "copy": { + "name": "cosmosDataRoleAssigment", + "count": "[length(variables('cosmosContainerNameSuffixes'))]", + "mode": "serial", + "batchSize": 1 + }, + "type": "Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments", + "apiVersion": "2025-04-15", + "name": "[format('{0}/{1}', parameters('cosmosDbName'), guid(variables('cosmosDefaultSqlRoleDefinitionId'), parameters('cosmosDbName'), variables('cosmosContainerNameSuffixes')[copyIndex()], parameters('projectIdentityPrincipalId')))]", + "properties": { + "principalId": "[parameters('projectIdentityPrincipalId')]", + "roleDefinitionId": "[variables('cosmosDefaultSqlRoleDefinitionId')]", + "scope": "[format('{0}/dbs/enterprise_memory/colls/{1}-{2}', resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosDbName')), parameters('projectWorkspaceId'), variables('cosmosContainerNameSuffixes')[copyIndex()])]" + } + } + ] + } + }, + "dependsOn": [ + "capabilityHost", + "cosmosDbRoleAssignments", + "project" + ] + }, + "storageAccountContainerRoleAssignments": { + "condition": "[and(not(empty(parameters('storageAccountConnection'))), variables('createProjectCapabilityHostInternal'))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[take(format('module.project.role-assign.storageAccountDataPlane.{0}', parameters('name')), 64)]", + "subscriptionId": "[parameters('storageAccountConnection').subscriptionId]", + "resourceGroup": "[parameters('storageAccountConnection').resourceGroupName]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "storageAccountName": { + "value": "[parameters('storageAccountConnection').resourceName]" + }, + "projectIdentityPrincipalId": { + "value": "[reference('project', '2025-04-01-preview', 'full').identity.principalId]" + }, + "projectWorkspaceId": { + "value": "[format('{0}-{1}-{2}-{3}-{4}', if(greaterOrEquals(length(reference('project').internalId), 8), substring(reference('project').internalId, 0, 8), ''), if(greaterOrEquals(length(reference('project').internalId), 12), substring(reference('project').internalId, 8, 4), ''), if(greaterOrEquals(length(reference('project').internalId), 16), substring(reference('project').internalId, 12, 4), ''), if(greaterOrEquals(length(reference('project').internalId), 20), substring(reference('project').internalId, 16, 4), ''), if(greaterOrEquals(length(reference('project').internalId), 32), substring(reference('project').internalId, 20, 12), ''))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "12109249428053532616" + } + }, + "parameters": { + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Required. The name of the storage account." + } + }, + "projectIdentityPrincipalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the project identity." + } + }, + "projectWorkspaceId": { + "type": "string", + "metadata": { + "description": "Required. The project workspace ID." + } + } + }, + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.Storage/storageAccounts/{0}', parameters('storageAccountName'))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), parameters('storageAccountName'), parameters('projectIdentityPrincipalId'))]", + "properties": { + "principalId": "[parameters('projectIdentityPrincipalId')]", + "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", + "principalType": "ServicePrincipal", + "conditionVersion": "2.0", + "condition": "[replace(' (\n (\n !(ActionMatches{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read''})\n AND !(ActionMatches{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action''})\n AND !(ActionMatches{''Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write''})\n )\n OR\n (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase ''#projectWorkspaceId#''\n AND @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase ''*-azureml-agent'')\n )\n ', '#projectWorkspaceId#', parameters('projectWorkspaceId'))]" + } + } + ] + } + }, + "dependsOn": [ + "capabilityHost", + "cosmosDbSqlRoleAssignments", + "project", + "storageAccountRoleAssignments" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Azure Resource Group." + }, + "value": "[resourceGroup().name]" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "Resource ID of the Project." + }, + "value": "[resourceId('Microsoft.CognitiveServices/accounts/projects', parameters('accountName'), parameters('name'))]" + }, + "name": { + "type": "string", + "metadata": { + "description": "Name of the Project." + }, + "value": "[parameters('name')]" + }, + "displayName": { + "type": "string", + "metadata": { + "description": "Display name of the Project." + }, + "value": "[reference('project').displayName]" + }, + "desc": { + "type": "string", + "metadata": { + "description": "Description of the Project." + }, + "value": "[reference('project').description]" + } + } + } + }, + "dependsOn": [ + "aiSearch", + "cosmosDb", + "foundryAccount", + "keyVault", + "storageAccount" + ] + } + }, + "outputs": { + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Azure Resource Group." + }, + "value": "[resourceGroup().name]" + }, + "keyVaultName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Azure Key Vault." + }, + "value": "[if(parameters('includeAssociatedResources'), reference('keyVault').outputs.name.value, '')]" + }, + "aiServicesName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Azure AI Services account." + }, + "value": "[reference('foundryAccount').outputs.name.value]" + }, + "aiSearchName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Azure AI Search service." + }, + "value": "[if(parameters('includeAssociatedResources'), reference('aiSearch').outputs.name.value, '')]" + }, + "aiProjectName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Azure AI Project." + }, + "value": "[reference('foundryProject').outputs.name.value]" + }, + "storageAccountName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Azure Storage Account." + }, + "value": "[if(parameters('includeAssociatedResources'), reference('storageAccount').outputs.name.value, '')]" + }, + "cosmosAccountName": { + "type": "string", + "metadata": { + "description": "Name of the deployed Azure Cosmos DB account." + }, + "value": "[if(parameters('includeAssociatedResources'), reference('cosmosDb').outputs.name.value, '')]" + } + } + } + } + } + }, + "outputs": { + "aiProjectName": { + "type": "string", + "value": "[reference('aiFoundry').outputs.aiProjectName.value]" + }, + "aiServicesName": { + "type": "string", + "value": "[reference('aiFoundry').outputs.aiServicesName.value]" + }, + "location": { + "type": "string", + "value": "[parameters('location')]" + } + } + } + }, + "dependsOn": [ + "userAssignedIdentity" + ] + }, + "apiContainerApp": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('ca-api-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('apiContainerAppName')]" + }, + "location": { + "value": "[reference('containerAppsEnvironment').outputs.location.value]" + }, + "containerAppsEnvId": { + "value": "[reference('containerAppsEnvironment').outputs.resourceId.value]" + }, + "containerRegistryServer": { + "value": "[reference('containerRegistry').outputs.loginServer.value]" + }, + "managedIdentityId": { + "value": "[reference('userAssignedIdentity').outputs.resourceId.value]" + }, + "targetPort": { + "value": 8090 + }, + "externalIngress": { + "value": "[not(variables('isAILZIntegrated'))]" + }, + "corsEnabled": { + "value": true + }, + "livenessProbePath": { + "value": "/" + }, + "cpuCores": { + "value": 2 + }, + "memoryInGB": { + "value": "4Gi" + }, + "minReplicas": { + "value": 1 + }, + "maxReplicas": { + "value": 2 + }, + "environmentVariables": { + "value": [ + { + "name": "AZURE_APP_CONFIG_ENDPOINT", + "value": "[reference('appConfigStore').outputs.endpoint.value]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[if(not(empty(parameters('existingAppInsightsId'))), reference(parameters('existingAppInsightsId'), '2020-02-02').ConnectionString, if(variables('shouldCreateAppInsights'), reference('appInsights').outputs.connectionString.value, ''))]" + }, + { + "name": "AZURE_CLIENT_ID", + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" + } + ] + }, + "tags": { + "value": "[union(variables('tags'), createObject('azd-service-name', 'api'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "15394956397498485985" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the container app" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for the container app" + } + }, + "containerAppsEnvId": { + "type": "string", + "metadata": { + "description": "Container Apps Environment ID" + } + }, + "containerRegistryServer": { + "type": "string", + "metadata": { + "description": "Container Registry server" + } + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "Managed Identity Resource ID" + } + }, + "targetPort": { + "type": "int", + "metadata": { + "description": "Target port for the container" + } + }, + "externalIngress": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable external ingress. Defaults to true." + } + }, + "corsEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable CORS. Defaults to true." + } + }, + "cpuCores": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "CPU cores for the container. Defaults to 1." + } + }, + "memoryInGB": { + "type": "string", + "defaultValue": "2Gi", + "metadata": { + "description": "Memory in GB for the container. Defaults to 2Gi." + } + }, + "livenessProbePath": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Liveness probe path. Leave empty to disable liveness probe. Defaults to /health." + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Minimum number of replicas for scaling. Defaults to 1." + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 2, + "metadata": { + "description": "Maximum number of replicas for scaling. Defaults to 2." + } + }, + "environmentVariables": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Environment variables" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to resources" + } + }, + "placeholderImage": { + "type": "string", + "defaultValue": "mcr.microsoft.com/k8se/quickstart:latest", + "metadata": { + "description": "Placeholder image to use before azd deploy replaces it" + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}.containerApp', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "environmentResourceId": { + "value": "[parameters('containerAppsEnvId')]" + }, + "corsPolicy": "[if(parameters('corsEnabled'), createObject('value', createObject('allowedOrigins', createArray('*'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'), 'allowedHeaders', createArray('*'), 'exposeHeaders', createArray('*'), 'maxAge', 3600, 'allowCredentials', true())), createObject('value', null()))]", + "containers": { + "value": [ + { + "name": "[parameters('name')]", + "image": "[parameters('placeholderImage')]", + "resources": { + "cpu": "[parameters('cpuCores')]", + "memory": "[parameters('memoryInGB')]" + }, + "env": "[parameters('environmentVariables')]", + "probes": "[if(not(empty(parameters('livenessProbePath'))), createArray(createObject('type', 'Liveness', 'httpGet', createObject('path', parameters('livenessProbePath'), 'port', parameters('targetPort')), 'initialDelaySeconds', 5, 'periodSeconds', 60)), createArray())]" + } + ] + }, + "ingressAllowInsecure": { + "value": false + }, + "ingressExternal": { + "value": "[parameters('externalIngress')]" + }, + "ingressTargetPort": { + "value": "[parameters('targetPort')]" + }, + "ingressTransport": { + "value": "auto" + }, + "activeRevisionsMode": { + "value": "Single" + }, + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[parameters('managedIdentityId')]" + ] + } + }, + "registries": { + "value": [ + { + "server": "[parameters('containerRegistryServer')]", + "identity": "[parameters('managedIdentityId')]" + } + ] + }, + "scaleSettings": "[if(greater(parameters('maxReplicas'), 1), createObject('value', createObject('minReplicas', parameters('minReplicas'), 'maxReplicas', parameters('maxReplicas'), 'rules', createArray(createObject('name', 'http-scaler', 'http', createObject('metadata', createObject('concurrentRequests', '10')))))), createObject('value', null()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "7056981135113238663" + }, + "name": "Container Apps", + "description": "This module deploys a Container App." + }, + "definitions": { + "ingressPortMappingType": { + "type": "object", + "properties": { + "exposedPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the exposed port for the target port. If not specified, it defaults to target port." + } + }, + "external": { + "type": "bool", + "metadata": { + "description": "Required. Specifies whether the app port is accessible outside of the environment." + } + }, + "targetPort": { + "type": "int", + "metadata": { + "description": "Required. Specifies the port the container listens on." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an ingress port mapping." + } + }, + "serviceBindingType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the service." + } + }, + "serviceId": { + "type": "string", + "metadata": { + "description": "Required. The service ID." + } + } + }, + "metadata": { + "description": "The type for a service binding." + } + }, + "environmentVarType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Environment variable name." + } + }, + "secretRef": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the Container App secret from which to pull the environment variable value." + } + }, + "value": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Non-secret environment variable value." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an environment variable." + } + }, + "containerAppProbeType": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Optional. Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3." + } + }, + "httpGet": { + "$ref": "#/definitions/containerAppProbeHttpGetType", + "nullable": true, + "metadata": { + "description": "Optional. HTTPGet specifies the http request to perform." + } + }, + "initialDelaySeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 60, + "metadata": { + "description": "Optional. Number of seconds after the container has started before liveness probes are initiated." + } + }, + "periodSeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 240, + "metadata": { + "description": "Optional. How often (in seconds) to perform the probe. Default to 10 seconds." + } + }, + "successThreshold": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Optional. Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup." + } + }, + "tcpSocket": { + "$ref": "#/definitions/containerAppProbeTcpSocketType", + "nullable": true, + "metadata": { + "description": "Optional. The TCP socket specifies an action involving a TCP port. TCP hooks not yet supported." + } + }, + "terminationGracePeriodSeconds": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is an alpha field and requires enabling ProbeTerminationGracePeriod feature gate. Maximum value is 3600 seconds (1 hour)." + } + }, + "timeoutSeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 240, + "metadata": { + "description": "Optional. Number of seconds after which the probe times out. Defaults to 1 second." + } + }, + "type": { + "type": "string", + "allowedValues": [ + "Liveness", + "Readiness", + "Startup" + ], + "nullable": true, + "metadata": { + "description": "Optional. The type of probe." + } + } + }, + "metadata": { + "description": "The type for a container app probe." + } + }, + "corsPolicyType": { + "type": "object", + "properties": { + "allowCredentials": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Switch to determine whether the resource allows credentials." + } + }, + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-headers header." + } + }, + "allowedMethods": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-methods header." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-origins header." + } + }, + "exposeHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-expose-headers header." + } + }, + "maxAge": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-max-age header." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a CORS policy." + } + }, + "containerAppProbeHttpGetType": { + "type": "object", + "properties": { + "host": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Host name to connect to. Defaults to the pod IP." + } + }, + "httpHeaders": { + "type": "array", + "items": { + "$ref": "#/definitions/containerAppProbeHttpGetHeadersItemType" + }, + "nullable": true, + "metadata": { + "description": "Optional. HTTP headers to set in the request." + } + }, + "path": { + "type": "string", + "metadata": { + "description": "Required. Path to access on the HTTP server." + } + }, + "port": { + "type": "int", + "metadata": { + "description": "Required. Name or number of the port to access on the container." + } + }, + "scheme": { + "type": "string", + "allowedValues": [ + "HTTP", + "HTTPS" + ], + "nullable": true, + "metadata": { + "description": "Optional. Scheme to use for connecting to the host. Defaults to HTTP." + } + } + }, + "metadata": { + "description": "The type for a container app probe HTTP GET." + } + }, + "containerAppProbeHttpGetHeadersItemType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the header." + } + }, + "value": { + "type": "string", + "metadata": { + "description": "Required. Value of the header." + } + } + }, + "metadata": { + "description": "The type for a container app probe HTTP GET header." + } + }, + "containerAppProbeTcpSocketType": { + "type": "object", + "properties": { + "host": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Host name to connect to, defaults to the pod IP." + } + }, + "port": { + "type": "int", + "minValue": 1, + "maxValue": 65535, + "metadata": { + "description": "Required. Number of the port to access on the container. Name must be an IANA_SVC_NAME." + } + } + }, + "metadata": { + "description": "The type for a container app probe TCP socket." + } + }, + "scaleType": { + "type": "object", + "properties": { + "maxReplicas": { + "type": "int", + "metadata": { + "description": "Required. The maximum number of replicas." + } + }, + "minReplicas": { + "type": "int", + "metadata": { + "description": "Required. The minimum number of replicas." + } + }, + "cooldownPeriod": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The cooldown period in seconds." + } + }, + "pollingInterval": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The polling interval in seconds." + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/scaleRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The scaling rules." + } + } + }, + "metadata": { + "description": "The scale settings for the Container App." + } + }, + "scaleRuleType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the scaling rule." + } + }, + "custom": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The custom scaling rule." + } + }, + "azureQueue": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The Azure Queue based scaling rule." + } + }, + "http": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The HTTP requests based scaling rule." + } + }, + "tcp": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The TCP based scaling rule." + } + } + }, + "metadata": { + "description": "The scaling rules for the Container App." + } + }, + "volumeMountType": { + "type": "object", + "properties": { + "mountPath": { + "type": "string", + "metadata": { + "description": "Required. Path within the container at which the volume should be mounted.Must not contain ':'." + } + }, + "subPath": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root)." + } + }, + "volumeName": { + "type": "string", + "metadata": { + "description": "Required. This must match the Name of a Volume." + } + } + }, + "metadata": { + "description": "The type for a volume mount." + } + }, + "secretType": { + "type": "object", + "properties": { + "identity": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of a managed identity to authenticate with Azure Key Vault, or System to use a system-assigned identity." + } + }, + "keyVaultUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The URL of the Azure Key Vault secret referenced by the Container App. Required if `value` is null." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the container app secret." + } + }, + "value": { + "type": "securestring", + "nullable": true, + "metadata": { + "description": "Conditional. The container app secret value, if not fetched from the Key Vault. Required if `keyVaultUrl` is not null." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a secret." + } + }, + "authConfigType": { + "type": "object", + "properties": { + "encryptionSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/encryptionSettings" + }, + "description": "Optional. The configuration settings of the secrets references of encryption key and signing key for ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "globalValidation": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/globalValidation" + }, + "description": "Optional. The configuration settings that determines the validation flow of users using Service Authentication and/or Authorization." + }, + "nullable": true + }, + "httpSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/httpSettings" + }, + "description": "Optional. The configuration settings of the HTTP requests for authentication and authorization requests made against ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "identityProviders": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/identityProviders" + }, + "description": "Optional. The configuration settings of each of the identity providers used to configure ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "login": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/login" + }, + "description": "Optional. The configuration settings of the login flow of users using ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "platform": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/platform" + }, + "description": "Optional. The configuration settings of the platform of ContainerApp Service Authentication/Authorization." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the container app's authentication configuration." + } + }, + "diagnosticSettingMetricsOnlyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of diagnostic setting." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.4.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.4.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Container App." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "kind": { + "type": "string", + "defaultValue": "containerapps", + "allowedValues": [ + "containerapps", + "workflowapp", + "functionapp" + ], + "metadata": { + "description": "Optional. Metadata used to render different experiences for resources of the same type." + } + }, + "disableIngress": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Bool to disable all ingress traffic for the container app." + } + }, + "ingressExternal": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Bool indicating if the App exposes an external HTTP endpoint." + } + }, + "clientCertificateMode": { + "type": "string", + "defaultValue": "ignore", + "allowedValues": [ + "accept", + "ignore", + "require" + ], + "metadata": { + "description": "Optional. Client certificate mode for mTLS." + } + }, + "corsPolicy": { + "$ref": "#/definitions/corsPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Object userd to configure CORS policy." + } + }, + "stickySessionsAffinity": { + "type": "string", + "defaultValue": "none", + "allowedValues": [ + "none", + "sticky" + ], + "metadata": { + "description": "Optional. Bool indicating if the Container App should enable session affinity." + } + }, + "ingressTransport": { + "type": "string", + "defaultValue": "auto", + "allowedValues": [ + "auto", + "http", + "http2", + "tcp" + ], + "metadata": { + "description": "Optional. Ingress transport protocol." + } + }, + "service": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/service" + }, + "description": "Optional. Dev ContainerApp service type." + }, + "nullable": true + }, + "includeAddOns": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Toggle to include the service configuration." + } + }, + "additionalPortMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/ingressPortMappingType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Settings to expose additional ports on container app." + } + }, + "ingressAllowInsecure": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Bool indicating if HTTP connections to is allowed. If set to false HTTP connections are automatically redirected to HTTPS connections." + } + }, + "ingressTargetPort": { + "type": "int", + "defaultValue": 80, + "metadata": { + "description": "Optional. Target Port in containers for traffic from ingress." + } + }, + "scaleSettings": { + "$ref": "#/definitions/scaleType", + "defaultValue": { + "maxReplicas": 10, + "minReplicas": 3 + }, + "metadata": { + "description": "Optional. The scaling settings of the service." + } + }, + "serviceBinds": { + "type": "array", + "items": { + "$ref": "#/definitions/serviceBindingType" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of container app services bound to the app." + } + }, + "activeRevisionsMode": { + "type": "string", + "defaultValue": "Single", + "allowedValues": [ + "Multiple", + "Single" + ], + "metadata": { + "description": "Optional. Controls how active revisions are handled for the Container app." + } + }, + "environmentResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of environment." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "registries": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/registries" + }, + "description": "Optional. Collection of private container registry credentials for containers used by the Container app." + }, + "nullable": true + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "customDomains": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/ingress/properties/customDomains" + }, + "description": "Optional. Custom domain bindings for Container App hostnames." + }, + "nullable": true + }, + "exposedPort": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Exposed Port in containers for TCP traffic from ingress." + } + }, + "ipSecurityRestrictions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/ingress/properties/ipSecurityRestrictions" + }, + "description": "Optional. Rules to restrict incoming IP address." + }, + "nullable": true + }, + "trafficLabel": { + "type": "string", + "defaultValue": "label-1", + "metadata": { + "description": "Optional. Associates a traffic label with a revision. Label name should be consist of lower case alphanumeric characters or dashes." + } + }, + "trafficLatestRevision": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates that the traffic weight belongs to a latest stable revision." + } + }, + "trafficRevisionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a revision." + } + }, + "trafficWeight": { + "type": "int", + "defaultValue": 100, + "metadata": { + "description": "Optional. Traffic weight assigned to a revision." + } + }, + "dapr": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/dapr" + }, + "description": "Optional. Dapr configuration for the Container App." + }, + "nullable": true + }, + "identitySettings": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/identitySettings" + }, + "description": "Optional. Settings for Managed Identities that are assigned to the Container App. If a Managed Identity is not specified here, default settings will be used." + }, + "nullable": true + }, + "maxInactiveRevisions": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Max inactive revisions a Container App can have." + } + }, + "runtime": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/runtime" + }, + "description": "Optional. Runtime configuration for the Container App." + }, + "nullable": true + }, + "containers": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/containers" + }, + "description": "Required. List of container definitions for the Container App." + } + }, + "terminationGracePeriodSeconds": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The termination grace period for the container app." + } + }, + "initContainersTemplate": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/initContainers" + }, + "description": "Optional. List of specialized containers that run before app containers." + }, + "nullable": true + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/definitions/secretType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The secrets of the Container App." + } + }, + "revisionSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User friendly suffix that is appended to the revision name." + } + }, + "volumes": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/volumes" + }, + "description": "Optional. List of volume definitions for the Container App." + }, + "nullable": true + }, + "workloadProfileName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Workload profile name to pin for container app execution." + } + }, + "authConfig": { + "$ref": "#/definitions/authConfigType", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Container App Auth configs." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "ContainerApp Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ad2dd5fb-cd4b-4fd4-a9b6-4fed3630980b')]", + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.app-containerapp.{0}.{1}', replace('0.19.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "containerApp": { + "type": "Microsoft.App/containerApps", + "apiVersion": "2025-02-02-preview", + "name": "[parameters('name')]", + "tags": "[parameters('tags')]", + "kind": "[parameters('kind')]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "properties": { + "environmentId": "[parameters('environmentResourceId')]", + "workloadProfileName": "[parameters('workloadProfileName')]", + "template": { + "containers": "[parameters('containers')]", + "terminationGracePeriodSeconds": "[parameters('terminationGracePeriodSeconds')]", + "initContainers": "[if(not(empty(parameters('initContainersTemplate'))), parameters('initContainersTemplate'), null())]", + "revisionSuffix": "[parameters('revisionSuffix')]", + "scale": "[parameters('scaleSettings')]", + "serviceBinds": "[if(and(parameters('includeAddOns'), not(empty(parameters('serviceBinds')))), parameters('serviceBinds'), null())]", + "volumes": "[if(not(empty(parameters('volumes'))), parameters('volumes'), null())]" + }, + "configuration": { + "activeRevisionsMode": "[parameters('activeRevisionsMode')]", + "dapr": "[if(not(empty(parameters('dapr'))), parameters('dapr'), null())]", + "identitySettings": "[if(not(empty(parameters('identitySettings'))), parameters('identitySettings'), null())]", + "ingress": "[if(parameters('disableIngress'), null(), createObject('additionalPortMappings', parameters('additionalPortMappings'), 'allowInsecure', if(not(equals(parameters('ingressTransport'), 'tcp')), parameters('ingressAllowInsecure'), false()), 'customDomains', if(not(empty(parameters('customDomains'))), parameters('customDomains'), null()), 'corsPolicy', if(and(not(equals(parameters('corsPolicy'), null())), not(equals(parameters('ingressTransport'), 'tcp'))), createObject('allowCredentials', coalesce(tryGet(parameters('corsPolicy'), 'allowCredentials'), false()), 'allowedHeaders', coalesce(tryGet(parameters('corsPolicy'), 'allowedHeaders'), createArray()), 'allowedMethods', coalesce(tryGet(parameters('corsPolicy'), 'allowedMethods'), createArray()), 'allowedOrigins', coalesce(tryGet(parameters('corsPolicy'), 'allowedOrigins'), createArray()), 'exposeHeaders', coalesce(tryGet(parameters('corsPolicy'), 'exposeHeaders'), createArray()), 'maxAge', tryGet(parameters('corsPolicy'), 'maxAge')), null()), 'clientCertificateMode', if(not(equals(parameters('ingressTransport'), 'tcp')), parameters('clientCertificateMode'), null()), 'exposedPort', parameters('exposedPort'), 'external', parameters('ingressExternal'), 'ipSecurityRestrictions', if(not(empty(parameters('ipSecurityRestrictions'))), parameters('ipSecurityRestrictions'), null()), 'targetPort', parameters('ingressTargetPort'), 'stickySessions', createObject('affinity', parameters('stickySessionsAffinity')), 'traffic', if(not(equals(parameters('ingressTransport'), 'tcp')), createArray(createObject('label', parameters('trafficLabel'), 'latestRevision', parameters('trafficLatestRevision'), 'revisionName', parameters('trafficRevisionName'), 'weight', parameters('trafficWeight'))), null()), 'transport', parameters('ingressTransport')))]", + "service": "[if(and(parameters('includeAddOns'), not(empty(parameters('service')))), parameters('service'), null())]", + "maxInactiveRevisions": "[parameters('maxInactiveRevisions')]", + "registries": "[if(not(empty(parameters('registries'))), parameters('registries'), null())]", + "secrets": "[parameters('secrets')]", + "runtime": "[if(not(empty(parameters('runtime'))), parameters('runtime'), null())]" + } + } + }, + "containerApp_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerApp_roleAssignments": { + "copy": { + "name": "containerApp_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.App/containerApps', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerApp_diagnosticSettings": { + "copy": { + "name": "containerApp_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerAppAuthConfigs": { + "condition": "[not(empty(parameters('authConfig')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-auth-config', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "containerAppName": { + "value": "[parameters('name')]" + }, + "encryptionSettings": { + "value": "[tryGet(parameters('authConfig'), 'encryptionSettings')]" + }, + "globalValidation": { + "value": "[tryGet(parameters('authConfig'), 'globalValidation')]" + }, + "httpSettings": { + "value": "[tryGet(parameters('authConfig'), 'httpSettings')]" + }, + "identityProviders": { + "value": "[tryGet(parameters('authConfig'), 'identityProviders')]" + }, + "login": { + "value": "[tryGet(parameters('authConfig'), 'login')]" + }, + "platform": { + "value": "[tryGet(parameters('authConfig'), 'platform')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "12480411243596309951" + }, + "name": "Container App Auth Configs", + "description": "This module deploys Container App Auth Configs." + }, + "parameters": { + "containerAppName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Container App. Required if the template is used in a standalone deployment." + } + }, + "encryptionSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/encryptionSettings" + }, + "description": "Optional. The configuration settings of the secrets references of encryption key and signing key for ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "globalValidation": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/globalValidation" + }, + "description": "Optional. The configuration settings that determines the validation flow of users using Service Authentication and/or Authorization." + }, + "nullable": true + }, + "httpSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/httpSettings" + }, + "description": "Optional. The configuration settings of the HTTP requests for authentication and authorization requests made against ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "identityProviders": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/identityProviders" + }, + "description": "Optional. The configuration settings of each of the identity providers used to configure ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "login": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/login" + }, + "description": "Optional. The configuration settings of the login flow of users using ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "platform": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/platform" + }, + "description": "Optional. The configuration settings of the platform of ContainerApp Service Authentication/Authorization." + }, + "nullable": true + } + }, + "resources": { + "containerApp": { + "existing": true, + "type": "Microsoft.App/containerApps", + "apiVersion": "2025-02-02-preview", + "name": "[parameters('containerAppName')]" + }, + "containerAppAuthConfigs": { + "type": "Microsoft.App/containerApps/authConfigs", + "apiVersion": "2025-02-02-preview", + "name": "[format('{0}/{1}', parameters('containerAppName'), 'current')]", + "properties": { + "encryptionSettings": "[parameters('encryptionSettings')]", + "globalValidation": "[parameters('globalValidation')]", + "httpSettings": "[parameters('httpSettings')]", + "identityProviders": "[parameters('identityProviders')]", + "login": "[parameters('login')]", + "platform": "[parameters('platform')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the set of Container App Auth configs." + }, + "value": "current" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the set of Container App Auth configs." + }, + "value": "[resourceId('Microsoft.App/containerApps/authConfigs', parameters('containerAppName'), 'current')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group containing the set of Container App Auth configs." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "containerApp" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Container App." + }, + "value": "[resourceId('Microsoft.App/containerApps', parameters('name'))]" + }, + "fqdn": { + "type": "string", + "metadata": { + "description": "The configuration of ingress fqdn." + }, + "value": "[if(parameters('disableIngress'), 'IngressDisabled', reference('containerApp').configuration.ingress.fqdn)]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Container App was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the Container App." + }, + "value": "[parameters('name')]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('containerApp', '2025-02-02-preview', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('containerApp', '2025-02-02-preview', 'full').location]" + } + } + } + } + } + ], + "outputs": { + "resourceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}.containerApp', deployment().name)), '2025-04-01').outputs.resourceId.value]" + }, + "name": { + "type": "string", + "value": "[format('{0}.containerApp', deployment().name)]" + }, + "fqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}.containerApp', deployment().name)), '2025-04-01').outputs.fqdn.value]" + } + } + } + }, + "dependsOn": [ + "appConfigStore", + "appInsights", + "containerAppsEnvironment", + "containerRegistry", + "userAssignedIdentity" + ] + }, + "workerContainerApp": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('ca-worker-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('workerContainerAppName')]" + }, + "location": { + "value": "[reference('containerAppsEnvironment').outputs.location.value]" + }, + "containerAppsEnvId": { + "value": "[reference('containerAppsEnvironment').outputs.resourceId.value]" + }, + "containerRegistryServer": { + "value": "[reference('containerRegistry').outputs.loginServer.value]" + }, + "managedIdentityId": { + "value": "[reference('userAssignedIdentity').outputs.resourceId.value]" + }, + "targetPort": { + "value": "[parameters('workerContainerAppTargetPort')]" + }, + "externalIngress": { + "value": "[not(variables('isAILZIntegrated'))]" + }, + "corsEnabled": { + "value": true + }, + "livenessProbePath": { + "value": "/" + }, + "cpuCores": { + "value": 2 + }, + "memoryInGB": { + "value": "4Gi" + }, + "minReplicas": { + "value": 1 + }, + "maxReplicas": { + "value": 3 + }, + "environmentVariables": { + "value": [ + { + "name": "AZURE_APP_CONFIG_ENDPOINT", + "value": "[reference('appConfigStore').outputs.endpoint.value]" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[if(not(empty(parameters('existingAppInsightsId'))), reference(parameters('existingAppInsightsId'), '2020-02-02').ConnectionString, if(variables('shouldCreateAppInsights'), reference('appInsights').outputs.connectionString.value, ''))]" + }, + { + "name": "AZURE_CLIENT_ID", + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" + } + ] + }, + "tags": { + "value": "[union(variables('tags'), createObject('azd-service-name', 'worker'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "15394956397498485985" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the container app" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for the container app" + } + }, + "containerAppsEnvId": { + "type": "string", + "metadata": { + "description": "Container Apps Environment ID" + } + }, + "containerRegistryServer": { + "type": "string", + "metadata": { + "description": "Container Registry server" + } + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "Managed Identity Resource ID" + } + }, + "targetPort": { + "type": "int", + "metadata": { + "description": "Target port for the container" + } + }, + "externalIngress": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable external ingress. Defaults to true." + } + }, + "corsEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable CORS. Defaults to true." + } + }, + "cpuCores": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "CPU cores for the container. Defaults to 1." + } + }, + "memoryInGB": { + "type": "string", + "defaultValue": "2Gi", + "metadata": { + "description": "Memory in GB for the container. Defaults to 2Gi." + } + }, + "livenessProbePath": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Liveness probe path. Leave empty to disable liveness probe. Defaults to /health." + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Minimum number of replicas for scaling. Defaults to 1." + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 2, + "metadata": { + "description": "Maximum number of replicas for scaling. Defaults to 2." + } + }, + "environmentVariables": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Environment variables" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to resources" + } + }, + "placeholderImage": { + "type": "string", + "defaultValue": "mcr.microsoft.com/k8se/quickstart:latest", + "metadata": { + "description": "Placeholder image to use before azd deploy replaces it" + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}.containerApp', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "environmentResourceId": { + "value": "[parameters('containerAppsEnvId')]" + }, + "corsPolicy": "[if(parameters('corsEnabled'), createObject('value', createObject('allowedOrigins', createArray('*'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'), 'allowedHeaders', createArray('*'), 'exposeHeaders', createArray('*'), 'maxAge', 3600, 'allowCredentials', true())), createObject('value', null()))]", + "containers": { + "value": [ + { + "name": "[parameters('name')]", + "image": "[parameters('placeholderImage')]", + "resources": { + "cpu": "[parameters('cpuCores')]", + "memory": "[parameters('memoryInGB')]" + }, + "env": "[parameters('environmentVariables')]", + "probes": "[if(not(empty(parameters('livenessProbePath'))), createArray(createObject('type', 'Liveness', 'httpGet', createObject('path', parameters('livenessProbePath'), 'port', parameters('targetPort')), 'initialDelaySeconds', 5, 'periodSeconds', 60)), createArray())]" + } + ] + }, + "ingressAllowInsecure": { + "value": false + }, + "ingressExternal": { + "value": "[parameters('externalIngress')]" + }, + "ingressTargetPort": { + "value": "[parameters('targetPort')]" + }, + "ingressTransport": { + "value": "auto" + }, + "activeRevisionsMode": { + "value": "Single" + }, + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[parameters('managedIdentityId')]" + ] + } + }, + "registries": { + "value": [ + { + "server": "[parameters('containerRegistryServer')]", + "identity": "[parameters('managedIdentityId')]" + } + ] + }, + "scaleSettings": "[if(greater(parameters('maxReplicas'), 1), createObject('value', createObject('minReplicas', parameters('minReplicas'), 'maxReplicas', parameters('maxReplicas'), 'rules', createArray(createObject('name', 'http-scaler', 'http', createObject('metadata', createObject('concurrentRequests', '10')))))), createObject('value', null()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "7056981135113238663" + }, + "name": "Container Apps", + "description": "This module deploys a Container App." + }, + "definitions": { + "ingressPortMappingType": { + "type": "object", + "properties": { + "exposedPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the exposed port for the target port. If not specified, it defaults to target port." + } + }, + "external": { + "type": "bool", + "metadata": { + "description": "Required. Specifies whether the app port is accessible outside of the environment." + } + }, + "targetPort": { + "type": "int", + "metadata": { + "description": "Required. Specifies the port the container listens on." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an ingress port mapping." + } + }, + "serviceBindingType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the service." + } + }, + "serviceId": { + "type": "string", + "metadata": { + "description": "Required. The service ID." + } + } + }, + "metadata": { + "description": "The type for a service binding." + } + }, + "environmentVarType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Environment variable name." + } + }, + "secretRef": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the Container App secret from which to pull the environment variable value." + } + }, + "value": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Non-secret environment variable value." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an environment variable." + } + }, + "containerAppProbeType": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Optional. Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3." + } + }, + "httpGet": { + "$ref": "#/definitions/containerAppProbeHttpGetType", + "nullable": true, + "metadata": { + "description": "Optional. HTTPGet specifies the http request to perform." + } + }, + "initialDelaySeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 60, + "metadata": { + "description": "Optional. Number of seconds after the container has started before liveness probes are initiated." + } + }, + "periodSeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 240, + "metadata": { + "description": "Optional. How often (in seconds) to perform the probe. Default to 10 seconds." + } + }, + "successThreshold": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Optional. Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup." + } + }, + "tcpSocket": { + "$ref": "#/definitions/containerAppProbeTcpSocketType", + "nullable": true, + "metadata": { + "description": "Optional. The TCP socket specifies an action involving a TCP port. TCP hooks not yet supported." + } + }, + "terminationGracePeriodSeconds": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is an alpha field and requires enabling ProbeTerminationGracePeriod feature gate. Maximum value is 3600 seconds (1 hour)." + } + }, + "timeoutSeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 240, + "metadata": { + "description": "Optional. Number of seconds after which the probe times out. Defaults to 1 second." + } + }, + "type": { + "type": "string", + "allowedValues": [ + "Liveness", + "Readiness", + "Startup" + ], + "nullable": true, + "metadata": { + "description": "Optional. The type of probe." + } + } + }, + "metadata": { + "description": "The type for a container app probe." + } + }, + "corsPolicyType": { + "type": "object", + "properties": { + "allowCredentials": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Switch to determine whether the resource allows credentials." + } + }, + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-headers header." + } + }, + "allowedMethods": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-methods header." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-origins header." + } + }, + "exposeHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-expose-headers header." + } + }, + "maxAge": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-max-age header." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a CORS policy." + } + }, + "containerAppProbeHttpGetType": { + "type": "object", + "properties": { + "host": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Host name to connect to. Defaults to the pod IP." + } + }, + "httpHeaders": { + "type": "array", + "items": { + "$ref": "#/definitions/containerAppProbeHttpGetHeadersItemType" + }, + "nullable": true, + "metadata": { + "description": "Optional. HTTP headers to set in the request." + } + }, + "path": { + "type": "string", + "metadata": { + "description": "Required. Path to access on the HTTP server." + } + }, + "port": { + "type": "int", + "metadata": { + "description": "Required. Name or number of the port to access on the container." + } + }, + "scheme": { + "type": "string", + "allowedValues": [ + "HTTP", + "HTTPS" + ], + "nullable": true, + "metadata": { + "description": "Optional. Scheme to use for connecting to the host. Defaults to HTTP." + } + } + }, + "metadata": { + "description": "The type for a container app probe HTTP GET." + } + }, + "containerAppProbeHttpGetHeadersItemType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the header." + } + }, + "value": { + "type": "string", + "metadata": { + "description": "Required. Value of the header." + } + } + }, + "metadata": { + "description": "The type for a container app probe HTTP GET header." + } + }, + "containerAppProbeTcpSocketType": { + "type": "object", + "properties": { + "host": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Host name to connect to, defaults to the pod IP." + } + }, + "port": { + "type": "int", + "minValue": 1, + "maxValue": 65535, + "metadata": { + "description": "Required. Number of the port to access on the container. Name must be an IANA_SVC_NAME." + } + } + }, + "metadata": { + "description": "The type for a container app probe TCP socket." + } + }, + "scaleType": { + "type": "object", + "properties": { + "maxReplicas": { + "type": "int", + "metadata": { + "description": "Required. The maximum number of replicas." + } + }, + "minReplicas": { + "type": "int", + "metadata": { + "description": "Required. The minimum number of replicas." + } + }, + "cooldownPeriod": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The cooldown period in seconds." + } + }, + "pollingInterval": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The polling interval in seconds." + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/scaleRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The scaling rules." + } + } + }, + "metadata": { + "description": "The scale settings for the Container App." + } + }, + "scaleRuleType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the scaling rule." + } + }, + "custom": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The custom scaling rule." + } + }, + "azureQueue": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The Azure Queue based scaling rule." + } + }, + "http": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The HTTP requests based scaling rule." + } + }, + "tcp": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The TCP based scaling rule." + } + } + }, + "metadata": { + "description": "The scaling rules for the Container App." + } + }, + "volumeMountType": { + "type": "object", + "properties": { + "mountPath": { + "type": "string", + "metadata": { + "description": "Required. Path within the container at which the volume should be mounted.Must not contain ':'." + } + }, + "subPath": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root)." + } + }, + "volumeName": { + "type": "string", + "metadata": { + "description": "Required. This must match the Name of a Volume." + } + } + }, + "metadata": { + "description": "The type for a volume mount." + } + }, + "secretType": { + "type": "object", + "properties": { + "identity": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of a managed identity to authenticate with Azure Key Vault, or System to use a system-assigned identity." + } + }, + "keyVaultUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The URL of the Azure Key Vault secret referenced by the Container App. Required if `value` is null." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the container app secret." + } + }, + "value": { + "type": "securestring", + "nullable": true, + "metadata": { + "description": "Conditional. The container app secret value, if not fetched from the Key Vault. Required if `keyVaultUrl` is not null." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a secret." + } + }, + "authConfigType": { + "type": "object", + "properties": { + "encryptionSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/encryptionSettings" + }, + "description": "Optional. The configuration settings of the secrets references of encryption key and signing key for ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "globalValidation": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/globalValidation" + }, + "description": "Optional. The configuration settings that determines the validation flow of users using Service Authentication and/or Authorization." + }, + "nullable": true + }, + "httpSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/httpSettings" + }, + "description": "Optional. The configuration settings of the HTTP requests for authentication and authorization requests made against ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "identityProviders": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/identityProviders" + }, + "description": "Optional. The configuration settings of each of the identity providers used to configure ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "login": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/login" + }, + "description": "Optional. The configuration settings of the login flow of users using ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "platform": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/platform" + }, + "description": "Optional. The configuration settings of the platform of ContainerApp Service Authentication/Authorization." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the container app's authentication configuration." + } + }, + "diagnosticSettingMetricsOnlyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of diagnostic setting." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.4.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.4.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Container App." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "kind": { + "type": "string", + "defaultValue": "containerapps", + "allowedValues": [ + "containerapps", + "workflowapp", + "functionapp" + ], + "metadata": { + "description": "Optional. Metadata used to render different experiences for resources of the same type." + } + }, + "disableIngress": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Bool to disable all ingress traffic for the container app." + } + }, + "ingressExternal": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Bool indicating if the App exposes an external HTTP endpoint." + } + }, + "clientCertificateMode": { + "type": "string", + "defaultValue": "ignore", + "allowedValues": [ + "accept", + "ignore", + "require" + ], + "metadata": { + "description": "Optional. Client certificate mode for mTLS." + } + }, + "corsPolicy": { + "$ref": "#/definitions/corsPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Object userd to configure CORS policy." + } + }, + "stickySessionsAffinity": { + "type": "string", + "defaultValue": "none", + "allowedValues": [ + "none", + "sticky" + ], + "metadata": { + "description": "Optional. Bool indicating if the Container App should enable session affinity." + } + }, + "ingressTransport": { + "type": "string", + "defaultValue": "auto", + "allowedValues": [ + "auto", + "http", + "http2", + "tcp" + ], + "metadata": { + "description": "Optional. Ingress transport protocol." + } + }, + "service": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/service" + }, + "description": "Optional. Dev ContainerApp service type." + }, + "nullable": true + }, + "includeAddOns": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Toggle to include the service configuration." + } + }, + "additionalPortMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/ingressPortMappingType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Settings to expose additional ports on container app." + } + }, + "ingressAllowInsecure": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Bool indicating if HTTP connections to is allowed. If set to false HTTP connections are automatically redirected to HTTPS connections." + } + }, + "ingressTargetPort": { + "type": "int", + "defaultValue": 80, + "metadata": { + "description": "Optional. Target Port in containers for traffic from ingress." + } + }, + "scaleSettings": { + "$ref": "#/definitions/scaleType", + "defaultValue": { + "maxReplicas": 10, + "minReplicas": 3 + }, + "metadata": { + "description": "Optional. The scaling settings of the service." + } + }, + "serviceBinds": { + "type": "array", + "items": { + "$ref": "#/definitions/serviceBindingType" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of container app services bound to the app." + } + }, + "activeRevisionsMode": { + "type": "string", + "defaultValue": "Single", + "allowedValues": [ + "Multiple", + "Single" + ], + "metadata": { + "description": "Optional. Controls how active revisions are handled for the Container app." + } + }, + "environmentResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of environment." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "registries": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/registries" + }, + "description": "Optional. Collection of private container registry credentials for containers used by the Container app." + }, + "nullable": true + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "customDomains": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/ingress/properties/customDomains" + }, + "description": "Optional. Custom domain bindings for Container App hostnames." + }, + "nullable": true + }, + "exposedPort": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Exposed Port in containers for TCP traffic from ingress." + } + }, + "ipSecurityRestrictions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/ingress/properties/ipSecurityRestrictions" + }, + "description": "Optional. Rules to restrict incoming IP address." + }, + "nullable": true + }, + "trafficLabel": { + "type": "string", + "defaultValue": "label-1", + "metadata": { + "description": "Optional. Associates a traffic label with a revision. Label name should be consist of lower case alphanumeric characters or dashes." + } + }, + "trafficLatestRevision": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates that the traffic weight belongs to a latest stable revision." + } + }, + "trafficRevisionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a revision." + } + }, + "trafficWeight": { + "type": "int", + "defaultValue": 100, + "metadata": { + "description": "Optional. Traffic weight assigned to a revision." + } + }, + "dapr": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/dapr" + }, + "description": "Optional. Dapr configuration for the Container App." + }, + "nullable": true + }, + "identitySettings": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/identitySettings" + }, + "description": "Optional. Settings for Managed Identities that are assigned to the Container App. If a Managed Identity is not specified here, default settings will be used." + }, + "nullable": true + }, + "maxInactiveRevisions": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Max inactive revisions a Container App can have." + } + }, + "runtime": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/runtime" + }, + "description": "Optional. Runtime configuration for the Container App." + }, + "nullable": true + }, + "containers": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/containers" + }, + "description": "Required. List of container definitions for the Container App." + } + }, + "terminationGracePeriodSeconds": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The termination grace period for the container app." + } + }, + "initContainersTemplate": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/initContainers" + }, + "description": "Optional. List of specialized containers that run before app containers." + }, + "nullable": true + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/definitions/secretType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The secrets of the Container App." + } + }, + "revisionSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User friendly suffix that is appended to the revision name." + } + }, + "volumes": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/volumes" + }, + "description": "Optional. List of volume definitions for the Container App." + }, + "nullable": true + }, + "workloadProfileName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Workload profile name to pin for container app execution." + } + }, + "authConfig": { + "$ref": "#/definitions/authConfigType", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Container App Auth configs." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "ContainerApp Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ad2dd5fb-cd4b-4fd4-a9b6-4fed3630980b')]", + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.app-containerapp.{0}.{1}', replace('0.19.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "containerApp": { + "type": "Microsoft.App/containerApps", + "apiVersion": "2025-02-02-preview", + "name": "[parameters('name')]", + "tags": "[parameters('tags')]", + "kind": "[parameters('kind')]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "properties": { + "environmentId": "[parameters('environmentResourceId')]", + "workloadProfileName": "[parameters('workloadProfileName')]", + "template": { + "containers": "[parameters('containers')]", + "terminationGracePeriodSeconds": "[parameters('terminationGracePeriodSeconds')]", + "initContainers": "[if(not(empty(parameters('initContainersTemplate'))), parameters('initContainersTemplate'), null())]", + "revisionSuffix": "[parameters('revisionSuffix')]", + "scale": "[parameters('scaleSettings')]", + "serviceBinds": "[if(and(parameters('includeAddOns'), not(empty(parameters('serviceBinds')))), parameters('serviceBinds'), null())]", + "volumes": "[if(not(empty(parameters('volumes'))), parameters('volumes'), null())]" + }, + "configuration": { + "activeRevisionsMode": "[parameters('activeRevisionsMode')]", + "dapr": "[if(not(empty(parameters('dapr'))), parameters('dapr'), null())]", + "identitySettings": "[if(not(empty(parameters('identitySettings'))), parameters('identitySettings'), null())]", + "ingress": "[if(parameters('disableIngress'), null(), createObject('additionalPortMappings', parameters('additionalPortMappings'), 'allowInsecure', if(not(equals(parameters('ingressTransport'), 'tcp')), parameters('ingressAllowInsecure'), false()), 'customDomains', if(not(empty(parameters('customDomains'))), parameters('customDomains'), null()), 'corsPolicy', if(and(not(equals(parameters('corsPolicy'), null())), not(equals(parameters('ingressTransport'), 'tcp'))), createObject('allowCredentials', coalesce(tryGet(parameters('corsPolicy'), 'allowCredentials'), false()), 'allowedHeaders', coalesce(tryGet(parameters('corsPolicy'), 'allowedHeaders'), createArray()), 'allowedMethods', coalesce(tryGet(parameters('corsPolicy'), 'allowedMethods'), createArray()), 'allowedOrigins', coalesce(tryGet(parameters('corsPolicy'), 'allowedOrigins'), createArray()), 'exposeHeaders', coalesce(tryGet(parameters('corsPolicy'), 'exposeHeaders'), createArray()), 'maxAge', tryGet(parameters('corsPolicy'), 'maxAge')), null()), 'clientCertificateMode', if(not(equals(parameters('ingressTransport'), 'tcp')), parameters('clientCertificateMode'), null()), 'exposedPort', parameters('exposedPort'), 'external', parameters('ingressExternal'), 'ipSecurityRestrictions', if(not(empty(parameters('ipSecurityRestrictions'))), parameters('ipSecurityRestrictions'), null()), 'targetPort', parameters('ingressTargetPort'), 'stickySessions', createObject('affinity', parameters('stickySessionsAffinity')), 'traffic', if(not(equals(parameters('ingressTransport'), 'tcp')), createArray(createObject('label', parameters('trafficLabel'), 'latestRevision', parameters('trafficLatestRevision'), 'revisionName', parameters('trafficRevisionName'), 'weight', parameters('trafficWeight'))), null()), 'transport', parameters('ingressTransport')))]", + "service": "[if(and(parameters('includeAddOns'), not(empty(parameters('service')))), parameters('service'), null())]", + "maxInactiveRevisions": "[parameters('maxInactiveRevisions')]", + "registries": "[if(not(empty(parameters('registries'))), parameters('registries'), null())]", + "secrets": "[parameters('secrets')]", + "runtime": "[if(not(empty(parameters('runtime'))), parameters('runtime'), null())]" + } + } + }, + "containerApp_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerApp_roleAssignments": { + "copy": { + "name": "containerApp_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.App/containerApps', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerApp_diagnosticSettings": { + "copy": { + "name": "containerApp_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerAppAuthConfigs": { + "condition": "[not(empty(parameters('authConfig')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-auth-config', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "containerAppName": { + "value": "[parameters('name')]" + }, + "encryptionSettings": { + "value": "[tryGet(parameters('authConfig'), 'encryptionSettings')]" + }, + "globalValidation": { + "value": "[tryGet(parameters('authConfig'), 'globalValidation')]" + }, + "httpSettings": { + "value": "[tryGet(parameters('authConfig'), 'httpSettings')]" + }, + "identityProviders": { + "value": "[tryGet(parameters('authConfig'), 'identityProviders')]" + }, + "login": { + "value": "[tryGet(parameters('authConfig'), 'login')]" + }, + "platform": { + "value": "[tryGet(parameters('authConfig'), 'platform')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "12480411243596309951" + }, + "name": "Container App Auth Configs", + "description": "This module deploys Container App Auth Configs." + }, + "parameters": { + "containerAppName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Container App. Required if the template is used in a standalone deployment." + } + }, + "encryptionSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/encryptionSettings" + }, + "description": "Optional. The configuration settings of the secrets references of encryption key and signing key for ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "globalValidation": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/globalValidation" + }, + "description": "Optional. The configuration settings that determines the validation flow of users using Service Authentication and/or Authorization." + }, + "nullable": true + }, + "httpSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/httpSettings" + }, + "description": "Optional. The configuration settings of the HTTP requests for authentication and authorization requests made against ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "identityProviders": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/identityProviders" + }, + "description": "Optional. The configuration settings of each of the identity providers used to configure ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "login": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/login" + }, + "description": "Optional. The configuration settings of the login flow of users using ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "platform": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/platform" + }, + "description": "Optional. The configuration settings of the platform of ContainerApp Service Authentication/Authorization." + }, + "nullable": true + } + }, + "resources": { + "containerApp": { + "existing": true, + "type": "Microsoft.App/containerApps", + "apiVersion": "2025-02-02-preview", + "name": "[parameters('containerAppName')]" + }, + "containerAppAuthConfigs": { + "type": "Microsoft.App/containerApps/authConfigs", + "apiVersion": "2025-02-02-preview", + "name": "[format('{0}/{1}', parameters('containerAppName'), 'current')]", + "properties": { + "encryptionSettings": "[parameters('encryptionSettings')]", + "globalValidation": "[parameters('globalValidation')]", + "httpSettings": "[parameters('httpSettings')]", + "identityProviders": "[parameters('identityProviders')]", + "login": "[parameters('login')]", + "platform": "[parameters('platform')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the set of Container App Auth configs." + }, + "value": "current" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the set of Container App Auth configs." + }, + "value": "[resourceId('Microsoft.App/containerApps/authConfigs', parameters('containerAppName'), 'current')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group containing the set of Container App Auth configs." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "containerApp" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Container App." + }, + "value": "[resourceId('Microsoft.App/containerApps', parameters('name'))]" + }, + "fqdn": { + "type": "string", + "metadata": { + "description": "The configuration of ingress fqdn." + }, + "value": "[if(parameters('disableIngress'), 'IngressDisabled', reference('containerApp').configuration.ingress.fqdn)]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Container App was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the Container App." + }, + "value": "[parameters('name')]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('containerApp', '2025-02-02-preview', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('containerApp', '2025-02-02-preview', 'full').location]" + } + } + } + } + } + ], + "outputs": { + "resourceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}.containerApp', deployment().name)), '2025-04-01').outputs.resourceId.value]" + }, + "name": { + "type": "string", + "value": "[format('{0}.containerApp', deployment().name)]" + }, + "fqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}.containerApp', deployment().name)), '2025-04-01').outputs.fqdn.value]" + } + } + } + }, + "dependsOn": [ + "appConfigStore", + "appInsights", + "containerAppsEnvironment", + "containerRegistry", + "userAssignedIdentity" + ] + }, + "webContainerApp": { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('ca-web-{0}', variables('resourceToken'))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[variables('webContainerAppName')]" + }, + "location": { + "value": "[reference('containerAppsEnvironment').outputs.location.value]" + }, + "containerAppsEnvId": { + "value": "[reference('containerAppsEnvironment').outputs.resourceId.value]" + }, + "containerRegistryServer": { + "value": "[reference('containerRegistry').outputs.loginServer.value]" + }, + "managedIdentityId": { + "value": "[reference('userAssignedIdentity').outputs.resourceId.value]" + }, + "targetPort": { + "value": 8080 + }, + "externalIngress": { + "value": "[not(variables('isAILZIntegrated'))]" + }, + "corsEnabled": { + "value": true + }, + "livenessProbePath": { + "value": "/" + }, + "cpuCores": { + "value": 1 + }, + "memoryInGB": { + "value": "2Gi" + }, + "minReplicas": { + "value": 1 + }, + "maxReplicas": { + "value": 1 + }, + "environmentVariables": { + "value": [ + { + "name": "VITE_API_BASE_URL", + "value": "[format('https://{0}/api/', reference('apiContainerApp').outputs.fqdn.value)]" + } + ] + }, + "tags": { + "value": "[union(variables('tags'), createObject('azd-service-name', 'web'))]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "15394956397498485985" + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Name of the container app" + } + }, + "location": { + "type": "string", + "metadata": { + "description": "Location for the container app" + } + }, + "containerAppsEnvId": { + "type": "string", + "metadata": { + "description": "Container Apps Environment ID" + } + }, + "containerRegistryServer": { + "type": "string", + "metadata": { + "description": "Container Registry server" + } + }, + "managedIdentityId": { + "type": "string", + "metadata": { + "description": "Managed Identity Resource ID" + } + }, + "targetPort": { + "type": "int", + "metadata": { + "description": "Target port for the container" + } + }, + "externalIngress": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable external ingress. Defaults to true." + } + }, + "corsEnabled": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Enable CORS. Defaults to true." + } + }, + "cpuCores": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "CPU cores for the container. Defaults to 1." + } + }, + "memoryInGB": { + "type": "string", + "defaultValue": "2Gi", + "metadata": { + "description": "Memory in GB for the container. Defaults to 2Gi." + } + }, + "livenessProbePath": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Liveness probe path. Leave empty to disable liveness probe. Defaults to /health." + } + }, + "minReplicas": { + "type": "int", + "defaultValue": 1, + "metadata": { + "description": "Minimum number of replicas for scaling. Defaults to 1." + } + }, + "maxReplicas": { + "type": "int", + "defaultValue": 2, + "metadata": { + "description": "Maximum number of replicas for scaling. Defaults to 2." + } + }, + "environmentVariables": { + "type": "array", + "defaultValue": [], + "metadata": { + "description": "Environment variables" + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to resources" + } + }, + "placeholderImage": { + "type": "string", + "defaultValue": "mcr.microsoft.com/k8se/quickstart:latest", + "metadata": { + "description": "Placeholder image to use before azd deploy replaces it" + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}.containerApp', deployment().name)]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "name": { + "value": "[parameters('name')]" + }, + "location": { + "value": "[parameters('location')]" + }, + "tags": { + "value": "[parameters('tags')]" + }, + "environmentResourceId": { + "value": "[parameters('containerAppsEnvId')]" + }, + "corsPolicy": "[if(parameters('corsEnabled'), createObject('value', createObject('allowedOrigins', createArray('*'), 'allowedMethods', createArray('GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'), 'allowedHeaders', createArray('*'), 'exposeHeaders', createArray('*'), 'maxAge', 3600, 'allowCredentials', true())), createObject('value', null()))]", + "containers": { + "value": [ + { + "name": "[parameters('name')]", + "image": "[parameters('placeholderImage')]", + "resources": { + "cpu": "[parameters('cpuCores')]", + "memory": "[parameters('memoryInGB')]" + }, + "env": "[parameters('environmentVariables')]", + "probes": "[if(not(empty(parameters('livenessProbePath'))), createArray(createObject('type', 'Liveness', 'httpGet', createObject('path', parameters('livenessProbePath'), 'port', parameters('targetPort')), 'initialDelaySeconds', 5, 'periodSeconds', 60)), createArray())]" + } + ] + }, + "ingressAllowInsecure": { + "value": false + }, + "ingressExternal": { + "value": "[parameters('externalIngress')]" + }, + "ingressTargetPort": { + "value": "[parameters('targetPort')]" + }, + "ingressTransport": { + "value": "auto" + }, + "activeRevisionsMode": { + "value": "Single" + }, + "managedIdentities": { + "value": { + "userAssignedResourceIds": [ + "[parameters('managedIdentityId')]" + ] + } + }, + "registries": { + "value": [ + { + "server": "[parameters('containerRegistryServer')]", + "identity": "[parameters('managedIdentityId')]" + } + ] + }, + "scaleSettings": "[if(greater(parameters('maxReplicas'), 1), createObject('value', createObject('minReplicas', parameters('minReplicas'), 'maxReplicas', parameters('maxReplicas'), 'rules', createArray(createObject('name', 'http-scaler', 'http', createObject('metadata', createObject('concurrentRequests', '10')))))), createObject('value', null()))]" + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "7056981135113238663" + }, + "name": "Container Apps", + "description": "This module deploys a Container App." + }, + "definitions": { + "ingressPortMappingType": { + "type": "object", + "properties": { + "exposedPort": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the exposed port for the target port. If not specified, it defaults to target port." + } + }, + "external": { + "type": "bool", + "metadata": { + "description": "Required. Specifies whether the app port is accessible outside of the environment." + } + }, + "targetPort": { + "type": "int", + "metadata": { + "description": "Required. Specifies the port the container listens on." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an ingress port mapping." + } + }, + "serviceBindingType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the service." + } + }, + "serviceId": { + "type": "string", + "metadata": { + "description": "Required. The service ID." + } + } + }, + "metadata": { + "description": "The type for a service binding." + } + }, + "environmentVarType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Environment variable name." + } + }, + "secretRef": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the Container App secret from which to pull the environment variable value." + } + }, + "value": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Non-secret environment variable value." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for an environment variable." + } + }, + "containerAppProbeType": { + "type": "object", + "properties": { + "failureThreshold": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Optional. Minimum consecutive failures for the probe to be considered failed after having succeeded. Defaults to 3." + } + }, + "httpGet": { + "$ref": "#/definitions/containerAppProbeHttpGetType", + "nullable": true, + "metadata": { + "description": "Optional. HTTPGet specifies the http request to perform." + } + }, + "initialDelaySeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 60, + "metadata": { + "description": "Optional. Number of seconds after the container has started before liveness probes are initiated." + } + }, + "periodSeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 240, + "metadata": { + "description": "Optional. How often (in seconds) to perform the probe. Default to 10 seconds." + } + }, + "successThreshold": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Optional. Minimum consecutive successes for the probe to be considered successful after having failed. Defaults to 1. Must be 1 for liveness and startup." + } + }, + "tcpSocket": { + "$ref": "#/definitions/containerAppProbeTcpSocketType", + "nullable": true, + "metadata": { + "description": "Optional. The TCP socket specifies an action involving a TCP port. TCP hooks not yet supported." + } + }, + "terminationGracePeriodSeconds": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Optional duration in seconds the pod needs to terminate gracefully upon probe failure. The grace period is the duration in seconds after the processes running in the pod are sent a termination signal and the time when the processes are forcibly halted with a kill signal. Set this value longer than the expected cleanup time for your process. If this value is nil, the pod's terminationGracePeriodSeconds will be used. Otherwise, this value overrides the value provided by the pod spec. Value must be non-negative integer. The value zero indicates stop immediately via the kill signal (no opportunity to shut down). This is an alpha field and requires enabling ProbeTerminationGracePeriod feature gate. Maximum value is 3600 seconds (1 hour)." + } + }, + "timeoutSeconds": { + "type": "int", + "nullable": true, + "minValue": 1, + "maxValue": 240, + "metadata": { + "description": "Optional. Number of seconds after which the probe times out. Defaults to 1 second." + } + }, + "type": { + "type": "string", + "allowedValues": [ + "Liveness", + "Readiness", + "Startup" + ], + "nullable": true, + "metadata": { + "description": "Optional. The type of probe." + } + } + }, + "metadata": { + "description": "The type for a container app probe." + } + }, + "corsPolicyType": { + "type": "object", + "properties": { + "allowCredentials": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Switch to determine whether the resource allows credentials." + } + }, + "allowedHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-headers header." + } + }, + "allowedMethods": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-methods header." + } + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-allow-origins header." + } + }, + "exposeHeaders": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-expose-headers header." + } + }, + "maxAge": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. Specifies the content for the access-control-max-age header." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a CORS policy." + } + }, + "containerAppProbeHttpGetType": { + "type": "object", + "properties": { + "host": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Host name to connect to. Defaults to the pod IP." + } + }, + "httpHeaders": { + "type": "array", + "items": { + "$ref": "#/definitions/containerAppProbeHttpGetHeadersItemType" + }, + "nullable": true, + "metadata": { + "description": "Optional. HTTP headers to set in the request." + } + }, + "path": { + "type": "string", + "metadata": { + "description": "Required. Path to access on the HTTP server." + } + }, + "port": { + "type": "int", + "metadata": { + "description": "Required. Name or number of the port to access on the container." + } + }, + "scheme": { + "type": "string", + "allowedValues": [ + "HTTP", + "HTTPS" + ], + "nullable": true, + "metadata": { + "description": "Optional. Scheme to use for connecting to the host. Defaults to HTTP." + } + } + }, + "metadata": { + "description": "The type for a container app probe HTTP GET." + } + }, + "containerAppProbeHttpGetHeadersItemType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the header." + } + }, + "value": { + "type": "string", + "metadata": { + "description": "Required. Value of the header." + } + } + }, + "metadata": { + "description": "The type for a container app probe HTTP GET header." + } + }, + "containerAppProbeTcpSocketType": { + "type": "object", + "properties": { + "host": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Host name to connect to, defaults to the pod IP." + } + }, + "port": { + "type": "int", + "minValue": 1, + "maxValue": 65535, + "metadata": { + "description": "Required. Number of the port to access on the container. Name must be an IANA_SVC_NAME." + } + } + }, + "metadata": { + "description": "The type for a container app probe TCP socket." + } + }, + "scaleType": { + "type": "object", + "properties": { + "maxReplicas": { + "type": "int", + "metadata": { + "description": "Required. The maximum number of replicas." + } + }, + "minReplicas": { + "type": "int", + "metadata": { + "description": "Required. The minimum number of replicas." + } + }, + "cooldownPeriod": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The cooldown period in seconds." + } + }, + "pollingInterval": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The polling interval in seconds." + } + }, + "rules": { + "type": "array", + "items": { + "$ref": "#/definitions/scaleRuleType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The scaling rules." + } + } + }, + "metadata": { + "description": "The scale settings for the Container App." + } + }, + "scaleRuleType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "metadata": { + "description": "Required. The name of the scaling rule." + } + }, + "custom": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The custom scaling rule." + } + }, + "azureQueue": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The Azure Queue based scaling rule." + } + }, + "http": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The HTTP requests based scaling rule." + } + }, + "tcp": { + "type": "object", + "nullable": true, + "metadata": { + "description": "Optional. The TCP based scaling rule." + } + } + }, + "metadata": { + "description": "The scaling rules for the Container App." + } + }, + "volumeMountType": { + "type": "object", + "properties": { + "mountPath": { + "type": "string", + "metadata": { + "description": "Required. Path within the container at which the volume should be mounted.Must not contain ':'." + } + }, + "subPath": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Path within the volume from which the container's volume should be mounted. Defaults to \"\" (volume's root)." + } + }, + "volumeName": { + "type": "string", + "metadata": { + "description": "Required. This must match the Name of a Volume." + } + } + }, + "metadata": { + "description": "The type for a volume mount." + } + }, + "secretType": { + "type": "object", + "properties": { + "identity": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of a managed identity to authenticate with Azure Key Vault, or System to use a system-assigned identity." + } + }, + "keyVaultUrl": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Conditional. The URL of the Azure Key Vault secret referenced by the Container App. Required if `value` is null." + } + }, + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of the container app secret." + } + }, + "value": { + "type": "securestring", + "nullable": true, + "metadata": { + "description": "Conditional. The container app secret value, if not fetched from the Key Vault. Required if `keyVaultUrl` is not null." + } + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for a secret." + } + }, + "authConfigType": { + "type": "object", + "properties": { + "encryptionSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/encryptionSettings" + }, + "description": "Optional. The configuration settings of the secrets references of encryption key and signing key for ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "globalValidation": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/globalValidation" + }, + "description": "Optional. The configuration settings that determines the validation flow of users using Service Authentication and/or Authorization." + }, + "nullable": true + }, + "httpSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/httpSettings" + }, + "description": "Optional. The configuration settings of the HTTP requests for authentication and authorization requests made against ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "identityProviders": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/identityProviders" + }, + "description": "Optional. The configuration settings of each of the identity providers used to configure ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "login": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/login" + }, + "description": "Optional. The configuration settings of the login flow of users using ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "platform": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/platform" + }, + "description": "Optional. The configuration settings of the platform of ContainerApp Service Authentication/Authorization." + }, + "nullable": true + } + }, + "metadata": { + "__bicep_export!": true, + "description": "The type for the container app's authentication configuration." + } + }, + "diagnosticSettingMetricsOnlyType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name of diagnostic setting." + } + }, + "metricCategories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "category": { + "type": "string", + "metadata": { + "description": "Required. Name of a Diagnostic Metric category for a resource type this setting is applied to. Set to `AllMetrics` to collect all metrics." + } + }, + "enabled": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enable or disable the category explicitly. Default is `true`." + } + } + } + }, + "nullable": true, + "metadata": { + "description": "Optional. The name of metrics that will be streamed. \"allMetrics\" includes all possible metrics for the resource. Set to `[]` to disable metric collection." + } + }, + "logAnalyticsDestinationType": { + "type": "string", + "allowedValues": [ + "AzureDiagnostics", + "Dedicated" + ], + "nullable": true, + "metadata": { + "description": "Optional. A string indicating whether the export to Log Analytics should use the default destination type, i.e. AzureDiagnostics, or use a destination type." + } + }, + "workspaceResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "storageAccountResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "eventHubAuthorizationRuleResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to." + } + }, + "eventHubName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub." + } + }, + "marketplacePartnerResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The full ARM resource ID of the Marketplace resource to which you would like to send Diagnostic Logs." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a diagnostic setting. To be used if only metrics are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + }, + "notes": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the notes of the lock." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a lock.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.6.0" + } + } + }, + "managedIdentityAllType": { + "type": "object", + "properties": { + "systemAssigned": { + "type": "bool", + "nullable": true, + "metadata": { + "description": "Optional. Enables system assigned managed identity on the resource." + } + }, + "userAssignedResourceIds": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true, + "metadata": { + "description": "Optional. The resource ID(s) to assign to the resource. Required if a user assigned identity is used for encryption." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a managed identity configuration. To be used if both a system-assigned & user-assigned identities are supported by the resource provider.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.4.1" + } + } + }, + "roleAssignmentType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The name (as GUID) of the role assignment. If not provided, a GUID will be generated." + } + }, + "roleDefinitionIdOrName": { + "type": "string", + "metadata": { + "description": "Required. The role to assign. You can provide either the display name of the role definition, the role definition GUID, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'." + } + }, + "principalId": { + "type": "string", + "metadata": { + "description": "Required. The principal ID of the principal (user/group/identity) to assign the role to." + } + }, + "principalType": { + "type": "string", + "allowedValues": [ + "Device", + "ForeignGroup", + "Group", + "ServicePrincipal", + "User" + ], + "nullable": true, + "metadata": { + "description": "Optional. The principal type of the assigned principal ID." + } + }, + "description": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The description of the role assignment." + } + }, + "condition": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase \"foo_storage_container\"." + } + }, + "conditionVersion": { + "type": "string", + "allowedValues": [ + "2.0" + ], + "nullable": true, + "metadata": { + "description": "Optional. Version of the condition." + } + }, + "delegatedManagedIdentityResourceId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. The Resource Id of the delegated managed identity resource." + } + } + }, + "metadata": { + "description": "An AVM-aligned type for a role assignment.", + "__bicep_imported_from!": { + "sourceTemplate": "br:mcr.microsoft.com/bicep/avm/utl/types/avm-common-types:0.4.1" + } + } + } + }, + "parameters": { + "name": { + "type": "string", + "metadata": { + "description": "Required. Name of the Container App." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Optional. Location for all Resources." + } + }, + "kind": { + "type": "string", + "defaultValue": "containerapps", + "allowedValues": [ + "containerapps", + "workflowapp", + "functionapp" + ], + "metadata": { + "description": "Optional. Metadata used to render different experiences for resources of the same type." + } + }, + "disableIngress": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Bool to disable all ingress traffic for the container app." + } + }, + "ingressExternal": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Bool indicating if the App exposes an external HTTP endpoint." + } + }, + "clientCertificateMode": { + "type": "string", + "defaultValue": "ignore", + "allowedValues": [ + "accept", + "ignore", + "require" + ], + "metadata": { + "description": "Optional. Client certificate mode for mTLS." + } + }, + "corsPolicy": { + "$ref": "#/definitions/corsPolicyType", + "nullable": true, + "metadata": { + "description": "Optional. Object userd to configure CORS policy." + } + }, + "stickySessionsAffinity": { + "type": "string", + "defaultValue": "none", + "allowedValues": [ + "none", + "sticky" + ], + "metadata": { + "description": "Optional. Bool indicating if the Container App should enable session affinity." + } + }, + "ingressTransport": { + "type": "string", + "defaultValue": "auto", + "allowedValues": [ + "auto", + "http", + "http2", + "tcp" + ], + "metadata": { + "description": "Optional. Ingress transport protocol." + } + }, + "service": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/service" + }, + "description": "Optional. Dev ContainerApp service type." + }, + "nullable": true + }, + "includeAddOns": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Optional. Toggle to include the service configuration." + } + }, + "additionalPortMappings": { + "type": "array", + "items": { + "$ref": "#/definitions/ingressPortMappingType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Settings to expose additional ports on container app." + } + }, + "ingressAllowInsecure": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Bool indicating if HTTP connections to is allowed. If set to false HTTP connections are automatically redirected to HTTPS connections." + } + }, + "ingressTargetPort": { + "type": "int", + "defaultValue": 80, + "metadata": { + "description": "Optional. Target Port in containers for traffic from ingress." + } + }, + "scaleSettings": { + "$ref": "#/definitions/scaleType", + "defaultValue": { + "maxReplicas": 10, + "minReplicas": 3 + }, + "metadata": { + "description": "Optional. The scaling settings of the service." + } + }, + "serviceBinds": { + "type": "array", + "items": { + "$ref": "#/definitions/serviceBindingType" + }, + "nullable": true, + "metadata": { + "description": "Optional. List of container app services bound to the app." + } + }, + "activeRevisionsMode": { + "type": "string", + "defaultValue": "Single", + "allowedValues": [ + "Multiple", + "Single" + ], + "metadata": { + "description": "Optional. Controls how active revisions are handled for the Container app." + } + }, + "environmentResourceId": { + "type": "string", + "metadata": { + "description": "Required. Resource ID of environment." + } + }, + "lock": { + "$ref": "#/definitions/lockType", + "nullable": true, + "metadata": { + "description": "Optional. The lock settings of the service." + } + }, + "tags": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/tags" + }, + "description": "Optional. Tags of the resource." + }, + "nullable": true + }, + "registries": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/registries" + }, + "description": "Optional. Collection of private container registry credentials for containers used by the Container app." + }, + "nullable": true + }, + "managedIdentities": { + "$ref": "#/definitions/managedIdentityAllType", + "nullable": true, + "metadata": { + "description": "Optional. The managed identity definition for this resource." + } + }, + "roleAssignments": { + "type": "array", + "items": { + "$ref": "#/definitions/roleAssignmentType" + }, + "nullable": true, + "metadata": { + "description": "Optional. Array of role assignments to create." + } + }, + "enableTelemetry": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Enable/Disable usage telemetry for module." + } + }, + "customDomains": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/ingress/properties/customDomains" + }, + "description": "Optional. Custom domain bindings for Container App hostnames." + }, + "nullable": true + }, + "exposedPort": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Exposed Port in containers for TCP traffic from ingress." + } + }, + "ipSecurityRestrictions": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/ingress/properties/ipSecurityRestrictions" + }, + "description": "Optional. Rules to restrict incoming IP address." + }, + "nullable": true + }, + "trafficLabel": { + "type": "string", + "defaultValue": "label-1", + "metadata": { + "description": "Optional. Associates a traffic label with a revision. Label name should be consist of lower case alphanumeric characters or dashes." + } + }, + "trafficLatestRevision": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Optional. Indicates that the traffic weight belongs to a latest stable revision." + } + }, + "trafficRevisionName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Name of a revision." + } + }, + "trafficWeight": { + "type": "int", + "defaultValue": 100, + "metadata": { + "description": "Optional. Traffic weight assigned to a revision." + } + }, + "dapr": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/dapr" + }, + "description": "Optional. Dapr configuration for the Container App." + }, + "nullable": true + }, + "identitySettings": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/identitySettings" + }, + "description": "Optional. Settings for Managed Identities that are assigned to the Container App. If a Managed Identity is not specified here, default settings will be used." + }, + "nullable": true + }, + "maxInactiveRevisions": { + "type": "int", + "defaultValue": 0, + "metadata": { + "description": "Optional. Max inactive revisions a Container App can have." + } + }, + "runtime": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/configuration/properties/runtime" + }, + "description": "Optional. Runtime configuration for the Container App." + }, + "nullable": true + }, + "containers": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/containers" + }, + "description": "Required. List of container definitions for the Container App." + } + }, + "terminationGracePeriodSeconds": { + "type": "int", + "nullable": true, + "metadata": { + "description": "Optional. The termination grace period for the container app." + } + }, + "initContainersTemplate": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/initContainers" + }, + "description": "Optional. List of specialized containers that run before app containers." + }, + "nullable": true + }, + "secrets": { + "type": "array", + "items": { + "$ref": "#/definitions/secretType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The secrets of the Container App." + } + }, + "revisionSuffix": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. User friendly suffix that is appended to the revision name." + } + }, + "volumes": { + "type": "array", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps@2025-02-02-preview#properties/properties/properties/template/properties/volumes" + }, + "description": "Optional. List of volume definitions for the Container App." + }, + "nullable": true + }, + "workloadProfileName": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Workload profile name to pin for container app execution." + } + }, + "authConfig": { + "$ref": "#/definitions/authConfigType", + "nullable": true, + "metadata": { + "description": "Optional. The name of the Container App Auth configs." + } + }, + "diagnosticSettings": { + "type": "array", + "items": { + "$ref": "#/definitions/diagnosticSettingMetricsOnlyType" + }, + "nullable": true, + "metadata": { + "description": "Optional. The diagnostic settings of the service." + } + } + }, + "variables": { + "copy": [ + { + "name": "formattedRoleAssignments", + "count": "[length(coalesce(parameters('roleAssignments'), createArray()))]", + "input": "[union(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')], createObject('roleDefinitionId', coalesce(tryGet(variables('builtInRoleNames'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName), if(contains(coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, '/providers/Microsoft.Authorization/roleDefinitions/'), coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', coalesce(parameters('roleAssignments'), createArray())[copyIndex('formattedRoleAssignments')].roleDefinitionIdOrName)))))]" + } + ], + "formattedUserAssignedIdentities": "[reduce(map(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createArray()), lambda('id', createObject(format('{0}', lambdaVariables('id')), createObject()))), createObject(), lambda('cur', 'next', union(lambdaVariables('cur'), lambdaVariables('next'))))]", + "identity": "[if(not(empty(parameters('managedIdentities'))), createObject('type', if(coalesce(tryGet(parameters('managedIdentities'), 'systemAssigned'), false()), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'SystemAssigned,UserAssigned', 'SystemAssigned'), if(not(empty(coalesce(tryGet(parameters('managedIdentities'), 'userAssignedResourceIds'), createObject()))), 'UserAssigned', 'None')), 'userAssignedIdentities', if(not(empty(variables('formattedUserAssignedIdentities'))), variables('formattedUserAssignedIdentities'), null())), null())]", + "builtInRoleNames": { + "ContainerApp Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ad2dd5fb-cd4b-4fd4-a9b6-4fed3630980b')]", + "Contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]", + "Owner": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]", + "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", + "Role Based Access Control Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]", + "User Access Administrator": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9')]" + } + }, + "resources": { + "avmTelemetry": { + "condition": "[parameters('enableTelemetry')]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2024-03-01", + "name": "[format('46d3xbcp.res.app-containerapp.{0}.{1}', replace('0.19.0', '.', '-'), substring(uniqueString(deployment().name, parameters('location')), 0, 4))]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "telemetry": { + "type": "String", + "value": "For more information, see https://aka.ms/avm/TelemetryInfo" + } + } + } + } + }, + "containerApp": { + "type": "Microsoft.App/containerApps", + "apiVersion": "2025-02-02-preview", + "name": "[parameters('name')]", + "tags": "[parameters('tags')]", + "kind": "[parameters('kind')]", + "location": "[parameters('location')]", + "identity": "[variables('identity')]", + "properties": { + "environmentId": "[parameters('environmentResourceId')]", + "workloadProfileName": "[parameters('workloadProfileName')]", + "template": { + "containers": "[parameters('containers')]", + "terminationGracePeriodSeconds": "[parameters('terminationGracePeriodSeconds')]", + "initContainers": "[if(not(empty(parameters('initContainersTemplate'))), parameters('initContainersTemplate'), null())]", + "revisionSuffix": "[parameters('revisionSuffix')]", + "scale": "[parameters('scaleSettings')]", + "serviceBinds": "[if(and(parameters('includeAddOns'), not(empty(parameters('serviceBinds')))), parameters('serviceBinds'), null())]", + "volumes": "[if(not(empty(parameters('volumes'))), parameters('volumes'), null())]" + }, + "configuration": { + "activeRevisionsMode": "[parameters('activeRevisionsMode')]", + "dapr": "[if(not(empty(parameters('dapr'))), parameters('dapr'), null())]", + "identitySettings": "[if(not(empty(parameters('identitySettings'))), parameters('identitySettings'), null())]", + "ingress": "[if(parameters('disableIngress'), null(), createObject('additionalPortMappings', parameters('additionalPortMappings'), 'allowInsecure', if(not(equals(parameters('ingressTransport'), 'tcp')), parameters('ingressAllowInsecure'), false()), 'customDomains', if(not(empty(parameters('customDomains'))), parameters('customDomains'), null()), 'corsPolicy', if(and(not(equals(parameters('corsPolicy'), null())), not(equals(parameters('ingressTransport'), 'tcp'))), createObject('allowCredentials', coalesce(tryGet(parameters('corsPolicy'), 'allowCredentials'), false()), 'allowedHeaders', coalesce(tryGet(parameters('corsPolicy'), 'allowedHeaders'), createArray()), 'allowedMethods', coalesce(tryGet(parameters('corsPolicy'), 'allowedMethods'), createArray()), 'allowedOrigins', coalesce(tryGet(parameters('corsPolicy'), 'allowedOrigins'), createArray()), 'exposeHeaders', coalesce(tryGet(parameters('corsPolicy'), 'exposeHeaders'), createArray()), 'maxAge', tryGet(parameters('corsPolicy'), 'maxAge')), null()), 'clientCertificateMode', if(not(equals(parameters('ingressTransport'), 'tcp')), parameters('clientCertificateMode'), null()), 'exposedPort', parameters('exposedPort'), 'external', parameters('ingressExternal'), 'ipSecurityRestrictions', if(not(empty(parameters('ipSecurityRestrictions'))), parameters('ipSecurityRestrictions'), null()), 'targetPort', parameters('ingressTargetPort'), 'stickySessions', createObject('affinity', parameters('stickySessionsAffinity')), 'traffic', if(not(equals(parameters('ingressTransport'), 'tcp')), createArray(createObject('label', parameters('trafficLabel'), 'latestRevision', parameters('trafficLatestRevision'), 'revisionName', parameters('trafficRevisionName'), 'weight', parameters('trafficWeight'))), null()), 'transport', parameters('ingressTransport')))]", + "service": "[if(and(parameters('includeAddOns'), not(empty(parameters('service')))), parameters('service'), null())]", + "maxInactiveRevisions": "[parameters('maxInactiveRevisions')]", + "registries": "[if(not(empty(parameters('registries'))), parameters('registries'), null())]", + "secrets": "[parameters('secrets')]", + "runtime": "[if(not(empty(parameters('runtime'))), parameters('runtime'), null())]" + } + } + }, + "containerApp_lock": { + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", + "type": "Microsoft.Authorization/locks", + "apiVersion": "2020-05-01", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", + "properties": { + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[coalesce(tryGet(parameters('lock'), 'notes'), if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.'))]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerApp_roleAssignments": { + "copy": { + "name": "containerApp_roleAssignments", + "count": "[length(coalesce(variables('formattedRoleAssignments'), createArray()))]" + }, + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'name'), guid(resourceId('Microsoft.App/containerApps', parameters('name')), coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId, coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId))]", + "properties": { + "roleDefinitionId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].roleDefinitionId]", + "principalId": "[coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()].principalId]", + "description": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'description')]", + "principalType": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'principalType')]", + "condition": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition')]", + "conditionVersion": "[if(not(empty(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'condition'))), coalesce(tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'conditionVersion'), '2.0'), null())]", + "delegatedManagedIdentityResourceId": "[tryGet(coalesce(variables('formattedRoleAssignments'), createArray())[copyIndex()], 'delegatedManagedIdentityResourceId')]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerApp_diagnosticSettings": { + "copy": { + "name": "containerApp_diagnosticSettings", + "count": "[length(coalesce(parameters('diagnosticSettings'), createArray()))]" + }, + "type": "Microsoft.Insights/diagnosticSettings", + "apiVersion": "2021-05-01-preview", + "scope": "[format('Microsoft.App/containerApps/{0}', parameters('name'))]", + "name": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'name'), format('{0}-diagnosticSettings', parameters('name')))]", + "properties": { + "copy": [ + { + "name": "metrics", + "count": "[length(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics'))))]", + "input": { + "category": "[coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')].category]", + "enabled": "[coalesce(tryGet(coalesce(tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'metricCategories'), createArray(createObject('category', 'AllMetrics')))[copyIndex('metrics')], 'enabled'), true())]", + "timeGrain": null + } + } + ], + "storageAccountId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'storageAccountResourceId')]", + "workspaceId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'workspaceResourceId')]", + "eventHubAuthorizationRuleId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubAuthorizationRuleResourceId')]", + "eventHubName": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'eventHubName')]", + "marketplacePartnerId": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'marketplacePartnerResourceId')]", + "logAnalyticsDestinationType": "[tryGet(coalesce(parameters('diagnosticSettings'), createArray())[copyIndex()], 'logAnalyticsDestinationType')]" + }, + "dependsOn": [ + "containerApp" + ] + }, + "containerAppAuthConfigs": { + "condition": "[not(empty(parameters('authConfig')))]", + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "[format('{0}-auth-config', uniqueString(deployment().name, parameters('location')))]", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "containerAppName": { + "value": "[parameters('name')]" + }, + "encryptionSettings": { + "value": "[tryGet(parameters('authConfig'), 'encryptionSettings')]" + }, + "globalValidation": { + "value": "[tryGet(parameters('authConfig'), 'globalValidation')]" + }, + "httpSettings": { + "value": "[tryGet(parameters('authConfig'), 'httpSettings')]" + }, + "identityProviders": { + "value": "[tryGet(parameters('authConfig'), 'identityProviders')]" + }, + "login": { + "value": "[tryGet(parameters('authConfig'), 'login')]" + }, + "platform": { + "value": "[tryGet(parameters('authConfig'), 'platform')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "12480411243596309951" + }, + "name": "Container App Auth Configs", + "description": "This module deploys Container App Auth Configs." + }, + "parameters": { + "containerAppName": { + "type": "string", + "metadata": { + "description": "Conditional. The name of the parent Container App. Required if the template is used in a standalone deployment." + } + }, + "encryptionSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/encryptionSettings" + }, + "description": "Optional. The configuration settings of the secrets references of encryption key and signing key for ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "globalValidation": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/globalValidation" + }, + "description": "Optional. The configuration settings that determines the validation flow of users using Service Authentication and/or Authorization." + }, + "nullable": true + }, + "httpSettings": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/httpSettings" + }, + "description": "Optional. The configuration settings of the HTTP requests for authentication and authorization requests made against ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "identityProviders": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/identityProviders" + }, + "description": "Optional. The configuration settings of each of the identity providers used to configure ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "login": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/login" + }, + "description": "Optional. The configuration settings of the login flow of users using ContainerApp Service Authentication/Authorization." + }, + "nullable": true + }, + "platform": { + "type": "object", + "metadata": { + "__bicep_resource_derived_type!": { + "source": "Microsoft.App/containerApps/authConfigs@2025-02-02-preview#properties/properties/properties/platform" + }, + "description": "Optional. The configuration settings of the platform of ContainerApp Service Authentication/Authorization." + }, + "nullable": true + } + }, + "resources": { + "containerApp": { + "existing": true, + "type": "Microsoft.App/containerApps", + "apiVersion": "2025-02-02-preview", + "name": "[parameters('containerAppName')]" + }, + "containerAppAuthConfigs": { + "type": "Microsoft.App/containerApps/authConfigs", + "apiVersion": "2025-02-02-preview", + "name": "[format('{0}/{1}', parameters('containerAppName'), 'current')]", + "properties": { + "encryptionSettings": "[parameters('encryptionSettings')]", + "globalValidation": "[parameters('globalValidation')]", + "httpSettings": "[parameters('httpSettings')]", + "identityProviders": "[parameters('identityProviders')]", + "login": "[parameters('login')]", + "platform": "[parameters('platform')]" + } + } + }, + "outputs": { + "name": { + "type": "string", + "metadata": { + "description": "The name of the set of Container App Auth configs." + }, + "value": "current" + }, + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the set of Container App Auth configs." + }, + "value": "[resourceId('Microsoft.App/containerApps/authConfigs', parameters('containerAppName'), 'current')]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The resource group containing the set of Container App Auth configs." + }, + "value": "[resourceGroup().name]" + } + } + } + }, + "dependsOn": [ + "containerApp" + ] + } + }, + "outputs": { + "resourceId": { + "type": "string", + "metadata": { + "description": "The resource ID of the Container App." + }, + "value": "[resourceId('Microsoft.App/containerApps', parameters('name'))]" + }, + "fqdn": { + "type": "string", + "metadata": { + "description": "The configuration of ingress fqdn." + }, + "value": "[if(parameters('disableIngress'), 'IngressDisabled', reference('containerApp').configuration.ingress.fqdn)]" + }, + "resourceGroupName": { + "type": "string", + "metadata": { + "description": "The name of the resource group the Container App was deployed into." + }, + "value": "[resourceGroup().name]" + }, + "name": { + "type": "string", + "metadata": { + "description": "The name of the Container App." + }, + "value": "[parameters('name')]" + }, + "systemAssignedMIPrincipalId": { + "type": "string", + "nullable": true, + "metadata": { + "description": "The principal ID of the system assigned identity." + }, + "value": "[tryGet(tryGet(reference('containerApp', '2025-02-02-preview', 'full'), 'identity'), 'principalId')]" + }, + "location": { + "type": "string", + "metadata": { + "description": "The location the resource was deployed into." + }, + "value": "[reference('containerApp', '2025-02-02-preview', 'full').location]" + } + } + } + } + } + ], + "outputs": { + "resourceId": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}.containerApp', deployment().name)), '2025-04-01').outputs.resourceId.value]" + }, + "name": { + "type": "string", + "value": "[format('{0}.containerApp', deployment().name)]" + }, + "fqdn": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Resources/deployments', format('{0}.containerApp', deployment().name)), '2025-04-01').outputs.fqdn.value]" + } + } + } + }, + "dependsOn": [ + "apiContainerApp", + "containerAppsEnvironment", + "containerRegistry", + "userAssignedIdentity" + ] + } + }, + "outputs": { + "DEPLOYMENT_MODE": { + "type": "string", + "value": "[parameters('deploymentMode')]" + }, + "AZURE_LOCATION": { + "type": "string", + "value": "[parameters('location')]" + }, + "AZURE_TENANT_ID": { + "type": "string", + "value": "[tenant().tenantId]" + }, + "AZURE_RESOURCE_GROUP": { + "type": "string", + "value": "[resourceGroup().name]" + }, + "AZURE_CONTAINER_REGISTRY_ENDPOINT": { + "type": "string", + "value": "[reference('containerRegistry').outputs.loginServer.value]" + }, + "AZURE_CONTAINER_REGISTRY_NAME": { + "type": "string", + "value": "[reference('containerRegistry').outputs.name.value]" + }, + "API_ENDPOINT": { + "type": "string", + "value": "[format('https://{0}', reference('apiContainerApp').outputs.fqdn.value)]" + }, + "WORKER_ENDPOINT": { + "type": "string", + "value": "[format('https://{0}', reference('workerContainerApp').outputs.fqdn.value)]" + }, + "WEB_ENDPOINT": { + "type": "string", + "value": "[format('https://{0}', reference('webContainerApp').outputs.fqdn.value)]" + }, + "COSMOS_DB_ENDPOINT": { + "type": "string", + "value": "[reference('cosmos').outputs.cosmosEndpoint.value]" + }, + "COSMOS_DB_NAME": { + "type": "string", + "value": "[parameters('cosmosDbName')]" + }, + "STORAGE_ACCOUNT_NAME": { + "type": "string", + "value": "[reference('storage').outputs.name.value]" + }, + "STORAGE_QUEUE_URL": { + "type": "string", + "value": "[reference('storage').outputs.primaryQueueEndpoint.value]" + }, + "STORAGE_QUEUE_NAME": { + "type": "string", + "value": "[parameters('workerQueueName')]" + }, + "APP_CONFIG_ENDPOINT": { + "type": "string", + "value": "[reference('appConfigStore').outputs.endpoint.value]" + }, + "APPLICATIONINSIGHTS_CONNECTION_STRING": { + "type": "string", + "value": "[if(not(empty(parameters('existingAppInsightsId'))), reference(parameters('existingAppInsightsId'), '2020-02-02').ConnectionString, if(variables('shouldCreateAppInsights'), reference('appInsights').outputs.connectionString.value, ''))]" + }, + "MANAGED_IDENTITY_CLIENT_ID": { + "type": "string", + "value": "[reference('userAssignedIdentity').outputs.clientId.value]" + }, + "MANAGED_IDENTITY_PRINCIPAL_ID": { + "type": "string", + "value": "[reference('userAssignedIdentity').outputs.principalId.value]" + }, + "AZURE_AI_FOUNDRY_LOCATION": { + "type": "string", + "value": "[reference('aiFoundry').outputs.location.value]" + }, + "AI_PROJECT_NAME": { + "type": "string", + "value": "[reference('aiFoundry').outputs.aiProjectName.value]" + }, + "AI_SERVICES_NAME": { + "type": "string", + "value": "[reference('aiFoundry').outputs.aiServicesName.value]" + }, + "VNET_RESOURCE_ID": { + "type": "string", + "value": "[if(variables('isAILZIntegrated'), parameters('existingVnetResourceId'), '')]" + }, + "PRIVATE_ENDPOINT_SUBNET_ID": { + "type": "string", + "value": "[if(variables('isAILZIntegrated'), variables('networkConfig').privateEndpointSubnetId, '')]" + }, + "CONTAINER_APPS_SUBNET_ID": { + "type": "string", + "value": "[if(variables('isAILZIntegrated'), variables('networkConfig').containerAppsSubnetId, '')]" + } + } +} \ No newline at end of file diff --git a/infra/bicep/main.parameters.json b/infra/bicep/main.parameters.json index 2d88b67..57ef45e 100644 --- a/infra/bicep/main.parameters.json +++ b/infra/bicep/main.parameters.json @@ -52,9 +52,6 @@ }, "existingAppInsightsId": { "value": "${EXISTING_APP_INSIGHTS_ID=}" - }, - "existingContainerRegistryResourceId": { - "value": "${EXISTING_CONTAINER_REGISTRY_RESOURCE_ID=}" } } } diff --git a/infra/bicep/modules/acr-role-assignment.bicep b/infra/bicep/modules/acr-role-assignment.bicep deleted file mode 100644 index bb3e232..0000000 --- a/infra/bicep/modules/acr-role-assignment.bicep +++ /dev/null @@ -1,33 +0,0 @@ -// ACR Role Assignment Module -// Assigns AcrPull and AcrPush roles to a managed identity on an existing Container Registry. -// Deployed as a cross-resource-group module when the existing ACR is in a different RG. - -@description('Name of the existing Container Registry') -param containerRegistryName string - -@description('Principal ID of the managed identity to assign roles to') -param principalId string - -resource existingAcr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = { - name: containerRegistryName -} - -resource acrPullRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(existingAcr.id, principalId, '7f951dda-4ed3-4680-a7ca-43fe172d538d') - scope: existingAcr - properties: { - principalId: principalId - principalType: 'ServicePrincipal' - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') // AcrPull - } -} - -resource acrPushRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - name: guid(existingAcr.id, principalId, '8311e382-0749-4cb8-b61a-304f252e45ec') - scope: existingAcr - properties: { - principalId: principalId - principalType: 'ServicePrincipal' - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8311e382-0749-4cb8-b61a-304f252e45ec') // AcrPush - } -} diff --git a/infra/bicep/modules/container-registry.bicep b/infra/bicep/modules/container-registry.bicep index eda929b..2c56143 100644 --- a/infra/bicep/modules/container-registry.bicep +++ b/infra/bicep/modules/container-registry.bicep @@ -32,6 +32,10 @@ param publicNetworkAccess string = 'Enabled' @allowed(['Enabled', 'Disabled']) param zoneRedundancy string = 'Disabled' +@description('Network rule bypass options - allows trusted Azure services to access the registry') +@allowed(['AzureServices', 'None']) +param networkRuleBypassOptions string = 'AzureServices' + @description('Managed Identity that will be given access to the Container Registry') param roleAssignedManagedIdentityPrincipalIds string[] @@ -72,6 +76,7 @@ module containerRegistry 'br:mcr.microsoft.com/bicep/avm/res/container-registry/ acrAdminUserEnabled: adminUserEnabled publicNetworkAccess: publicNetworkAccess zoneRedundancy: zoneRedundancy + networkRuleBypassOptions: networkRuleBypassOptions roleAssignments: concat(roleAssignmentsAcrPull, roleAssignmentsAcrPush, roleAssignmentsAcrDelete) privateEndpoints: enablePrivateEndpoint ? [ { diff --git a/infra/scripts/post-provision.sh b/infra/scripts/post-provision.sh index 96281c0..b8c9a49 100755 --- a/infra/scripts/post-provision.sh +++ b/infra/scripts/post-provision.sh @@ -12,10 +12,14 @@ echo "✓ Infrastructure provisioned successfully!" RESOURCE_GROUP=$(azd env get-value AZURE_RESOURCE_GROUP) STORAGE_ACCOUNT=$(azd env get-value STORAGE_ACCOUNT_NAME) COSMOS_ENDPOINT=$(azd env get-value COSMOS_DB_ENDPOINT) +DEPLOYMENT_MODE=$(azd env get-value DEPLOYMENT_MODE) +ACR_NAME=$(azd env get-value AZURE_CONTAINER_REGISTRY_NAME) echo "Resource Group: $RESOURCE_GROUP" echo "Storage Account: $STORAGE_ACCOUNT" echo "Cosmos DB Endpoint: $COSMOS_ENDPOINT" +echo "Deployment Mode: $DEPLOYMENT_MODE" +echo "Container Registry: $ACR_NAME" echo "✓ Creating storage queue (if not exists)..." QUEUE_NAME="contentflow-execution-requests" @@ -25,6 +29,28 @@ az storage queue create \ --auth-mode login \ --only-show-errors || echo "Queue already exists or error creating queue" +# Import placeholder image to ACR in AILZ mode (enables Container Apps provisioning without internet egress) +if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then + echo "✓ Importing placeholder image to ACR (AILZ mode - no internet egress)..." + echo " Source: mcr.microsoft.com/k8se/quickstart:latest" + echo " Target: $ACR_NAME/placeholder:latest" + + az acr import \ + --name "$ACR_NAME" \ + --source mcr.microsoft.com/k8se/quickstart:latest \ + --image placeholder:latest \ + --only-show-errors || { + echo "⚠ Warning: Failed to import placeholder image to ACR." + echo " This is not critical if the image is cached by Container Apps platform." + echo " If Container Apps fail to provision, you can manually import:" + echo " az acr import --name $ACR_NAME --source mcr.microsoft.com/k8se/quickstart:latest --image placeholder:latest" + } + + echo "✓ Placeholder image imported successfully" +else + echo "⊘ Skipping ACR import (basic mode - internet egress available)" +fi + echo "==================================================" echo "✓ Post-provision completed successfully" echo "==================================================" From 87cbfd7757b3992ce69d4115c26ec8bf0e6516c6 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 13:33:16 -0400 Subject: [PATCH 08/29] ACR issue fix 3 --- changes.md | 165 ++++++++++++++++++- infra/bicep/main.bicep | 9 + infra/bicep/modules/container-registry.bicep | 8 +- 3 files changed, 177 insertions(+), 5 deletions(-) diff --git a/changes.md b/changes.md index db61e83..1c813cc 100644 --- a/changes.md +++ b/changes.md @@ -5,7 +5,8 @@ - [1. Fix: LogAnalytics CustomerId Must Be a GUID](#1-fix-loganalytics-customerid-must-be-a-guid) - [2. Fix: Property Name Typo `privateEndpointsSubnetId`](#2-fix-property-name-typo-privateendpointssubnetid) - [3. Fix: `UnmatchedPrincipalType` – `deployer().objectId` Hardcoded as `User`](#3-fix-unmatchedprincipaltype--deployerobjectid-hardcoded-as-user) -- [4. Feature: Support Existing Container Registry from AI Landing Zone](#4-feature-support-existing-container-registry-from-ai-landing-zone) +- [4. Feature: Automated ACR Placeholder Import for AILZ Deployments](#4-feature-automated-acr-placeholder-import-for-ailz-deployments) +- [5. Fix: Container Apps Timeout – DNS Zone Group and Deployment Sequencing](#5-fix-container-apps-timeout--dns-zone-group-and-deployment-sequencing) --- @@ -246,3 +247,165 @@ fi ✅ **Works from jumpbox without Docker** — `az acr import` is server-side ✅ **Non-blocking** — ACA platform caches `k8se/quickstart`, provisioning succeeds even if import fails ✅ **CI/CD ready** — no human intervention required + +--- + +## 5. Fix: Container Apps Timeout – DNS Zone Group and Deployment Sequencing + +**Error:** + +``` +Container Apps deployment timing out after 20 minutes during AILZ-integrated deployments +``` + +**Symptoms:** + +- Container Apps (API, Worker, Web) fail to provision and timeout after 20 minutes +- ACR private endpoint exists and shows correct IP addresses (10.0.0.16 for login server, 10.0.0.15 for data endpoint) +- Private DNS Zone `privatelink.azurecr.io` is linked to VNet +- **BUT**: Private DNS Zone has NO A records for the ACR +- DNS zone group exists but shows empty configuration `{}` +- Without DNS resolution, Container Apps cannot resolve the private ACR FQDN +- Requests fall back to public DNS but ACR has `publicNetworkAccess: Disabled` → connection refused + +**Root Causes:** + +### Issue 1: DNS Zone Group Conditional Logic Bug + +In `infra/bicep/modules/container-registry.bicep` (lines 85-96), the conditional `!empty(acrPrivateDnsZoneId) ?` was placed **inside** the `privateDnsZoneGroupConfigs` array instead of wrapping the entire `privateDnsZoneGroups` array. This caused the AVM module to create a DNS zone group with an **empty configs array** `[]` when `acrPrivateDnsZoneId` was provided. + +An empty DNS zone group doesn't register A records in the Private DNS Zone, breaking DNS resolution for the private endpoint. + +**Before (BROKEN):** + +```bicep +privateDnsZoneGroups: [ + { + name: 'acr-dns-zone-group' + privateDnsZoneGroupConfigs: !empty(acrPrivateDnsZoneId) ? [ + { + name: 'acr-config' + privateDnsZoneResourceId: acrPrivateDnsZoneId + } + ] : [] + } +] +``` + +This creates: +- **When `acrPrivateDnsZoneId` is empty** (basic mode): `privateDnsZoneGroups: [{ name: 'acr-dns-zone-group', privateDnsZoneGroupConfigs: [] }]` → creates broken zone group +- **When `acrPrivateDnsZoneId` is provided** (AILZ mode): `privateDnsZoneGroups: [{ name: 'acr-dns-zone-group', privateDnsZoneGroupConfigs: [{...}] }]` → creates proper zone group + +Wait, that's backwards. Let me re-check the logic... + +Actually, when `acrPrivateDnsZoneId` is **provided** (AILZ mode), the condition evaluates to `true` and creates the config array. When **empty** (basic mode), it creates an empty array. But the zone group object itself is **always created** because the array wrapping it is unconditional. + +The problem is: when you don't provide a DNS zone ID (basic mode), it still creates a zone group with empty configs, which may cause deployment issues or doesn't properly configure DNS. + +**After (FIXED):** + +```bicep +privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [ + { + name: 'acr-dns-zone-group' + privateDnsZoneGroupConfigs: [ + { + name: 'acr-config' + privateDnsZoneResourceId: acrPrivateDnsZoneId + } + ] + } +] : [] +``` + +This creates: +- **When `acrPrivateDnsZoneId` is empty** (basic mode): `privateDnsZoneGroups: []` → no zone group created +- **When `acrPrivateDnsZoneId` is provided** (AILZ mode): `privateDnsZoneGroups: [{ name: 'acr-dns-zone-group', privateDnsZoneGroupConfigs: [{...}] }]` → proper zone group with configs + +### Issue 2: Container Apps Deployment Race Condition + +In `infra/bicep/main.bicep`, the Container Apps modules (`apiContainerApp`, `workerContainerApp`, `webContainerApp`) reference the ACR via `containerRegistry.outputs.loginServer` in their parameters. While Bicep can infer the dependency from this output reference, it only waits for the **ACR resource creation** to complete — not necessarily for all child resources like: + +- RBAC role assignments +- Private endpoint provisioning +- **DNS zone group registration** (which registers A records) + +In practice, Container Apps attempted to deploy before the DNS zone group had registered the A records, causing DNS resolution failures and 20-minute timeouts. + +**Solution:** Add explicit `dependsOn: [containerRegistry]` to all three Container Apps modules to ensure they wait for the **entire module** to complete, including all child resources. + +**Files Changed:** + +### `infra/bicep/modules/container-registry.bicep` + +**Lines 85-96** (DNS zone group conditional fix): + +```bicep +privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [ + { + name: 'acr-dns-zone-group' + privateDnsZoneGroupConfigs: [ + { + name: 'acr-config' + privateDnsZoneResourceId: acrPrivateDnsZoneId + } + ] + } +] : [] +``` + +### `infra/bicep/main.bicep` + +Added `dependsOn: [containerRegistry]` to: + +1. **API Container App** (after line 518): +```bicep +module apiContainerApp 'modules/container-app.bicep' = { + name: 'ca-api-${resourceToken}' + params: { /* ... */ } + dependsOn: [ + containerRegistry + ] +} +``` + +2. **Worker Container App** (after line 553): +```bicep +module workerContainerApp 'modules/container-app.bicep' = { + name: 'ca-worker-${resourceToken}' + params: { /* ... */ } + dependsOn: [ + containerRegistry + ] +} +``` + +3. **Web Container App** (after line 587): +```bicep +module webContainerApp 'modules/container-app.bicep' = { + name: 'ca-web-${resourceToken}' + params: { /* ... */ } + dependsOn: [ + containerRegistry + ] +} +``` + +**Note on Bicep Linter Warnings:** + +Bicep linter reports `no-unnecessary-dependson` warnings because it detects the dependency is implicit via `.outputs.loginServer`. However, the explicit `dependsOn` is **intentionally defensive** to ensure: + +- ACR resource is created +- RBAC role assignments complete +- Private endpoint provisions +- **DNS zone group registers A records** (critical for AILZ) + +The implicit dependency only guarantees the ACR resource exists, not that all child resources are ready. Given the DNS timing issues observed, the explicit `dependsOn` is the correct choice for reliable AILZ deployments. + +**Expected Results After Fix:** + +✅ Private DNS Zone automatically gets A records for ACR FQDN pointing to private IPs +✅ DNS resolution works correctly within the VNet +✅ Container Apps deploy successfully without timeout +✅ No manual DNS record creation needed +✅ Deployment completes in 15-20 minutes (down from 20+ minutes timeout) diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 0eddd91..5961f01 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -515,6 +515,9 @@ module apiContainerApp 'modules/container-app.bicep' = { ] tags: union(tags, { 'azd-service-name': 'api' }) } + dependsOn: [ + containerRegistry + ] } // ========== WORKER CONTAINER APP ========== @@ -550,6 +553,9 @@ module workerContainerApp 'modules/container-app.bicep' = { ] tags: union(tags, { 'azd-service-name': 'worker' }) } + dependsOn: [ + containerRegistry + ] } // ========== API CONTAINER APP ========== @@ -577,6 +583,9 @@ module webContainerApp 'modules/container-app.bicep' = { ] tags: union(tags, { 'azd-service-name': 'web' }) } + dependsOn: [ + containerRegistry + ] } // ========== OUTPUTS ========== diff --git a/infra/bicep/modules/container-registry.bicep b/infra/bicep/modules/container-registry.bicep index 2c56143..20495dc 100644 --- a/infra/bicep/modules/container-registry.bicep +++ b/infra/bicep/modules/container-registry.bicep @@ -85,17 +85,17 @@ module containerRegistry 'br:mcr.microsoft.com/bicep/avm/res/container-registry/ subnetResourceId: privateEndpointSubnetId service: 'registry' privateLinkServiceConnectionName: '${containerRegistryName}-acr-plsc' - privateDnsZoneGroups: [ + privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [ { name: 'acr-dns-zone-group' - privateDnsZoneGroupConfigs: !empty(acrPrivateDnsZoneId) ? [ + privateDnsZoneGroupConfigs: [ { name: 'acr-config' privateDnsZoneResourceId: acrPrivateDnsZoneId } - ] : [] + ] } - ] + ] : [] } ] : [] } From 4be9f1331c827d8d818e2f49b579303ffccbba3f Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 14:38:07 -0400 Subject: [PATCH 09/29] ACR issue fix 4 --- changes.md | 199 ++++++++++-------- infra/bicep/main.bicep | 16 ++ infra/bicep/modules/container-registry.bicep | 13 +- .../private-endpoint-dns-zone-group.bicep | 32 +++ 4 files changed, 156 insertions(+), 104 deletions(-) create mode 100644 infra/bicep/modules/private-endpoint-dns-zone-group.bicep diff --git a/changes.md b/changes.md index 1c813cc..17d03fe 100644 --- a/changes.md +++ b/changes.md @@ -250,7 +250,7 @@ fi --- -## 5. Fix: Container Apps Timeout – DNS Zone Group and Deployment Sequencing +## 5. Fix: Container Apps Timeout – DNS Zone Group Creation Workaround **Error:** @@ -260,86 +260,77 @@ Container Apps deployment timing out after 20 minutes during AILZ-integrated dep **Symptoms:** -- Container Apps (API, Worker, Web) fail to provision and timeout after 20 minutes +- Container Apps (API, Worker, Web) fail to provision and timeout after exactly 20 minutes - ACR private endpoint exists and shows correct IP addresses (10.0.0.16 for login server, 10.0.0.15 for data endpoint) - Private DNS Zone `privatelink.azurecr.io` is linked to VNet - **BUT**: Private DNS Zone has NO A records for the ACR -- DNS zone group exists but shows empty configuration `{}` +- DNS zone group does NOT exist (query returns `[]`) - Without DNS resolution, Container Apps cannot resolve the private ACR FQDN -- Requests fall back to public DNS but ACR has `publicNetworkAccess: Disabled` → connection refused +- Requests fall back to public DNS but ACR has `publicNetworkAccess: Disabled` → connection refused → timeout -**Root Causes:** - -### Issue 1: DNS Zone Group Conditional Logic Bug - -In `infra/bicep/modules/container-registry.bicep` (lines 85-96), the conditional `!empty(acrPrivateDnsZoneId) ?` was placed **inside** the `privateDnsZoneGroupConfigs` array instead of wrapping the entire `privateDnsZoneGroups` array. This caused the AVM module to create a DNS zone group with an **empty configs array** `[]` when `acrPrivateDnsZoneId` was provided. - -An empty DNS zone group doesn't register A records in the Private DNS Zone, breaking DNS resolution for the private endpoint. +**Root Cause:** -**Before (BROKEN):** +The Azure Verified Module (AVM) `containerregistry/registry:0.9.3` does not properly handle the `privateDnsZoneGroups` parameter when passed conditionally through our wrapper module. Even with the corrected conditional logic: ```bicep -privateDnsZoneGroups: [ - { - name: 'acr-dns-zone-group' - privateDnsZoneGroupConfigs: !empty(acrPrivateDnsZoneId) ? [ - { - name: 'acr-config' - privateDnsZoneResourceId: acrPrivateDnsZoneId - } - ] : [] - } -] +privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [{...}] : [] ``` -This creates: -- **When `acrPrivateDnsZoneId` is empty** (basic mode): `privateDnsZoneGroups: [{ name: 'acr-dns-zone-group', privateDnsZoneGroupConfigs: [] }]` → creates broken zone group -- **When `acrPrivateDnsZoneId` is provided** (AILZ mode): `privateDnsZoneGroups: [{ name: 'acr-dns-zone-group', privateDnsZoneGroupConfigs: [{...}] }]` → creates proper zone group +The AVM module fails to create the DNS zone group, resulting in NO A record registration in the Private DNS Zone. Investigation via Azure CLI confirmed: +- `az network private-endpoint dns-zone-group list` returned `[]` (no zone groups) +- `az network private-dns record-set a list` returned empty table (no A records) +- ACR deployment succeeded, private endpoint succeeded, but DNS registration failed -Wait, that's backwards. Let me re-check the logic... +**Solution: Explicit DNS Zone Group Module** -Actually, when `acrPrivateDnsZoneId` is **provided** (AILZ mode), the condition evaluates to `true` and creates the config array. When **empty** (basic mode), it creates an empty array. But the zone group object itself is **always created** because the array wrapping it is unconditional. +Instead of relying on the AVM module to create the DNS zone group, we now create it explicitly using a separate Bicep module that runs **after** the ACR and private endpoint are provisioned. -The problem is: when you don't provide a DNS zone ID (basic mode), it still creates a zone group with empty configs, which may cause deployment issues or doesn't properly configure DNS. +### Files Changed -**After (FIXED):** +#### **New File: `infra/bicep/modules/private-endpoint-dns-zone-group.bicep`** -```bicep -privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [ - { - name: 'acr-dns-zone-group' - privateDnsZoneGroupConfigs: [ - { - name: 'acr-config' - privateDnsZoneResourceId: acrPrivateDnsZoneId - } - ] - } -] : [] -``` +Created a dedicated module that explicitly creates a DNS zone group for an existing private endpoint: -This creates: -- **When `acrPrivateDnsZoneId` is empty** (basic mode): `privateDnsZoneGroups: []` → no zone group created -- **When `acrPrivateDnsZoneId` is provided** (AILZ mode): `privateDnsZoneGroups: [{ name: 'acr-dns-zone-group', privateDnsZoneGroupConfigs: [{...}] }]` → proper zone group with configs - -### Issue 2: Container Apps Deployment Race Condition +```bicep +@description('Name of the private endpoint') +param privateEndpointName string -In `infra/bicep/main.bicep`, the Container Apps modules (`apiContainerApp`, `workerContainerApp`, `webContainerApp`) reference the ACR via `containerRegistry.outputs.loginServer` in their parameters. While Bicep can infer the dependency from this output reference, it only waits for the **ACR resource creation** to complete — not necessarily for all child resources like: +@description('Resource ID of the private DNS zone') +param privateDnsZoneId string -- RBAC role assignments -- Private endpoint provisioning -- **DNS zone group registration** (which registers A records) +@description('Location for the deployment metadata') +param location string = resourceGroup().location -In practice, Container Apps attempted to deploy before the DNS zone group had registered the A records, causing DNS resolution failures and 20-minute timeouts. +// Reference existing private endpoint +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' existing = { + name: privateEndpointName +} -**Solution:** Add explicit `dependsOn: [containerRegistry]` to all three Container Apps modules to ensure they wait for the **entire module** to complete, including all child resources. +// Create DNS zone group for the private endpoint +resource dnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = { + parent: privateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: 'privatelink-azurecr-io' + properties: { + privateDnsZoneId: privateDnsZoneId + } + } + ] + } +} -**Files Changed:** +output dnsZoneGroupName string = dnsZoneGroup.name +output dnsZoneGroupId string = dnsZoneGroup.id +``` -### `infra/bicep/modules/container-registry.bicep` +#### **Modified: `infra/bicep/modules/container-registry.bicep`** -**Lines 85-96** (DNS zone group conditional fix): +Removed `privateDnsZoneGroups` from the `privateEndpoints` array passed to the AVM module: +**Before:** ```bicep privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [ { @@ -354,58 +345,80 @@ privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [ ] : [] ``` -### `infra/bicep/main.bicep` - -Added `dependsOn: [containerRegistry]` to: - -1. **API Container App** (after line 518): +**After:** ```bicep -module apiContainerApp 'modules/container-app.bicep' = { - name: 'ca-api-${resourceToken}' - params: { /* ... */ } - dependsOn: [ - containerRegistry - ] -} +// privateDnsZoneGroups removed - AVM module 0.9.3 does not handle conditional correctly +// DNS zone group will be created explicitly in main.bicep using separate module ``` -2. **Worker Container App** (after line 553): +#### **Modified: `infra/bicep/main.bicep`** + +Added explicit DNS zone group module **after** the ACR module: + ```bicep -module workerContainerApp 'modules/container-app.bicep' = { - name: 'ca-worker-${resourceToken}' - params: { /* ... */ } - dependsOn: [ - containerRegistry - ] +// ========== CONTAINER REGISTRY WITH PRIVATE ENDPOINT SUPPORT ========== +module containerRegistry 'modules/container-registry.bicep' = { + name: 'acr-${resourceToken}' + params: { + containerRegistryName: containerRegistryName + location: location + roleAssignedManagedIdentityPrincipalIds: [userAssignedIdentity.outputs.principalId] + enablePrivateEndpoint: isAILZIntegrated + privateEndpointSubnetId: isAILZIntegrated ? networkConfig.privateEndpointSubnetId : '' + acrPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.acr : '' + publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' + networkRuleBypassOptions: 'AzureServices' + tags: tags + } } -``` -3. **Web Container App** (after line 587): -```bicep -module webContainerApp 'modules/container-app.bicep' = { - name: 'ca-web-${resourceToken}' - params: { /* ... */ } +// ========== ACR DNS ZONE GROUP (EXPLICIT CREATION) ========== +// Create DNS zone group explicitly to ensure A records are registered in private DNS zone +// Workaround: AVM containerregistry module 0.9.3 does not properly handle privateDnsZoneGroups conditional +// This separate module creates the DNS zone group after the private endpoint exists +module acrDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'acr-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${containerRegistryName}-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.acr + location: location + } dependsOn: [ containerRegistry ] } ``` -**Note on Bicep Linter Warnings:** - -Bicep linter reports `no-unnecessary-dependson` warnings because it detects the dependency is implicit via `.outputs.loginServer`. However, the explicit `dependsOn` is **intentionally defensive** to ensure: +Also retained the `dependsOn: [containerRegistry]` on all three Container Apps modules (API, Worker, Web) to ensure they wait for both ACR and DNS zone group to complete. -- ACR resource is created -- RBAC role assignments complete -- Private endpoint provisions -- **DNS zone group registers A records** (critical for AILZ) +**How It Works:** -The implicit dependency only guarantees the ACR resource exists, not that all child resources are ready. Given the DNS timing issues observed, the explicit `dependsOn` is the correct choice for reliable AILZ deployments. +1. **ACR Module** creates the Container Registry with private endpoint (but no DNS zone group) +2. **DNS Zone Group Module** (conditional on AILZ mode) creates the DNS zone group explicitly +3. Azure automatically registers A records in the Private DNS Zone when the zone group is properly configured +4. **Container Apps** wait for the entire ACR deployment (including DNS) via explicit `dependsOn` +5. DNS resolution works correctly, Container Apps pull images successfully **Expected Results After Fix:** -✅ Private DNS Zone automatically gets A records for ACR FQDN pointing to private IPs +✅ Private DNS Zone automatically gets A records for ACR FQDN pointing to private IPs (10.0.0.x) +✅ DNS zone group exists and has proper configuration (not empty `{}` or missing `[]`) ✅ DNS resolution works correctly within the VNet -✅ Container Apps deploy successfully without timeout +✅ Container Apps deploy successfully without 20-minute timeout ✅ No manual DNS record creation needed -✅ Deployment completes in 15-20 minutes (down from 20+ minutes timeout) +✅ Deployment completes in 15-20 minutes total + +**Verification Commands:** + +```bash +# Verify DNS zone group exists +az network private-endpoint dns-zone-group list \ + --resource-group \ + --endpoint-name -pe + +# Verify A records registered +az network private-dns record-set a list \ + --resource-group \ + --zone-name privatelink.azurecr.io \ + --query "[].{Name:name, IP:aRecords[0].ipv4Address}" +``` diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 5961f01..c71cb7a 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -451,6 +451,22 @@ module containerRegistry 'modules/container-registry.bicep' = { } } +// ========== ACR DNS ZONE GROUP (EXPLICIT CREATION) ========== +// Create DNS zone group explicitly to ensure A records are registered in private DNS zone +// Workaround: AVM containerregistry module 0.9.3 does not properly handle privateDnsZoneGroups conditional +// This separate module creates the DNS zone group after the private endpoint exists +module acrDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'acr-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${containerRegistryName}-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.acr + location: location + } + dependsOn: [ + containerRegistry + ] +} + // ========== CONTAINER APPS ENVIRONMENT ========== // Create new Container Apps Environment, with private endpoint support (if using AILZ integrated mode), // or use public endpoints (basic mode) diff --git a/infra/bicep/modules/container-registry.bicep b/infra/bicep/modules/container-registry.bicep index 20495dc..77a872e 100644 --- a/infra/bicep/modules/container-registry.bicep +++ b/infra/bicep/modules/container-registry.bicep @@ -85,17 +85,8 @@ module containerRegistry 'br:mcr.microsoft.com/bicep/avm/res/container-registry/ subnetResourceId: privateEndpointSubnetId service: 'registry' privateLinkServiceConnectionName: '${containerRegistryName}-acr-plsc' - privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [ - { - name: 'acr-dns-zone-group' - privateDnsZoneGroupConfigs: [ - { - name: 'acr-config' - privateDnsZoneResourceId: acrPrivateDnsZoneId - } - ] - } - ] : [] + // privateDnsZoneGroups removed - AVM module 0.9.3 does not handle conditional correctly + // DNS zone group will be created explicitly in main.bicep using separate module } ] : [] } diff --git a/infra/bicep/modules/private-endpoint-dns-zone-group.bicep b/infra/bicep/modules/private-endpoint-dns-zone-group.bicep new file mode 100644 index 0000000..350c359 --- /dev/null +++ b/infra/bicep/modules/private-endpoint-dns-zone-group.bicep @@ -0,0 +1,32 @@ +@description('Name of the private endpoint') +param privateEndpointName string + +@description('Resource ID of the private DNS zone') +param privateDnsZoneId string + +@description('Location for the deployment metadata') +param location string = resourceGroup().location + +// Reference existing private endpoint +resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-11-01' existing = { + name: privateEndpointName +} + +// Create DNS zone group for the private endpoint +resource dnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2023-11-01' = { + parent: privateEndpoint + name: 'default' + properties: { + privateDnsZoneConfigs: [ + { + name: 'privatelink-azurecr-io' + properties: { + privateDnsZoneId: privateDnsZoneId + } + } + ] + } +} + +output dnsZoneGroupName string = dnsZoneGroup.name +output dnsZoneGroupId string = dnsZoneGroup.id From ecb12c7a84de3812eb6799b82c2a4733915ec13c Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 17:45:46 -0400 Subject: [PATCH 10/29] Fix: App Configuration Keys Failure - ARM Private Link Delegation --- changes.md | 213 ++++++++++++++++++++- infra/bicep/modules/app-config-store.bicep | 2 +- 2 files changed, 210 insertions(+), 5 deletions(-) diff --git a/changes.md b/changes.md index 17d03fe..b26ca3f 100644 --- a/changes.md +++ b/changes.md @@ -4,9 +4,10 @@ - [1. Fix: LogAnalytics CustomerId Must Be a GUID](#1-fix-loganalytics-customerid-must-be-a-guid) - [2. Fix: Property Name Typo `privateEndpointsSubnetId`](#2-fix-property-name-typo-privateendpointssubnetid) -- [3. Fix: `UnmatchedPrincipalType` – `deployer().objectId` Hardcoded as `User`](#3-fix-unmatchedprincipaltype--deployerobjectid-hardcoded-as-user) +- [3. Fix: `UnmatchedPrincipalType` - `deployer().objectId` Hardcoded as `User`](#3-fix-unmatchedprincipaltype---deployerobjectid-hardcoded-as-user) - [4. Feature: Automated ACR Placeholder Import for AILZ Deployments](#4-feature-automated-acr-placeholder-import-for-ailz-deployments) -- [5. Fix: Container Apps Timeout – DNS Zone Group and Deployment Sequencing](#5-fix-container-apps-timeout--dns-zone-group-and-deployment-sequencing) +- [5. Fix: Container Apps Timeout - DNS Zone Group Creation Workaround](#5-fix-container-apps-timeout---dns-zone-group-creation-workaround) +- [6. Fix: App Configuration Keys Failure - ARM Private Link Delegation](#6-fix-app-configuration-keys-failure---arm-private-link-delegation) --- @@ -79,7 +80,7 @@ Simple typo fix — `privateEndpointsSubnetId` → `privateEndpointSubnetId`. --- -## 3. Fix: `UnmatchedPrincipalType` – `deployer().objectId` Hardcoded as `User` +## 3. Fix: `UnmatchedPrincipalType` - `deployer().objectId` Hardcoded as `User` **Error:** @@ -250,7 +251,7 @@ fi --- -## 5. Fix: Container Apps Timeout – DNS Zone Group Creation Workaround +## 5. Fix: Container Apps Timeout - DNS Zone Group Creation Workaround **Error:** @@ -422,3 +423,207 @@ az network private-dns record-set a list \ --zone-name privatelink.azurecr.io \ --query "[].{Name:name, IP:aRecords[0].ipv4Address}" ``` + +--- + +**Fix Verification Results (Deployment: content_flow_Friday20-02)** + +**Tested**: March 20, 2026 +**Environment**: AILZ-integrated mode with jumpbox deployment + +✅ **DNS Zone Group Created Successfully** +```json +{ + "name": "default", + "privateDnsZoneConfigs": [ + { + "name": "privatelink-azurecr-io", + "privateDnsZoneId": "/subscriptions/.../privateDnsZones/privatelink.azurecr.io", + "recordSets": [ + { + "fqdn": "cra5s63braj5xj2.eastus2.data.privatelink.azurecr.io", + "ipAddresses": ["10.0.0.15"], + "provisioningState": "Succeeded", + "recordType": "A" + }, + { + "fqdn": "cra5s63braj5xj2.privatelink.azurecr.io", + "ipAddresses": ["10.0.0.16"], + "provisioningState": "Succeeded", + "recordType": "A" + } + ] + } + ], + "provisioningState": "Succeeded" +} +``` + +✅ **A Records Registered in Private DNS Zone** +``` +Name IP +--------------- --------- +cra5s63braj5xj2 10.0.0.16 +``` + +✅ **Container Apps Deployed Successfully** +- Worker: 16.4 seconds (previously: 20-minute timeout) +- API: 49.4 seconds (previously: 20-minute timeout) +- Web: 18.5 seconds (previously: 20-minute timeout) + +**Result**: The workaround with explicit DNS zone group module completely resolved the Container Apps timeout issue. DNS records are now automatically registered, and Container Apps can successfully pull images from the private ACR. + +--- + +## 6. Fix: App Configuration Keys Failure - ARM Private Link Delegation + +**Error:** + +``` +appConfigKeys-a5s63braj5xj2 → Failed +Error: Forbidden - Access to the requested resource is forbidden +``` + +**Root Cause:** + +In AILZ-integrated deployments with private endpoints, the App Configuration Store is configured with: +- `publicNetworkAccess: 'Disabled'` — no public internet access +- Private endpoint in the VNet — accessible only through private network +- `dataPlaneProxy.privateLinkDelegation: 'Disabled'` — **THIS IS THE PROBLEM** + +When the `appConfigStoreKeys` Bicep module tries to create key-value pairs using the ARM deployment service, **ARM cannot access the App Configuration data plane** because: + +1. ARM deployment service operates **outside the customer's VNet** +2. App Configuration has public access disabled (correct for AILZ) +3. ARM has no private endpoint connection to the customer's VNet +4. **Azure Resource Manager requires explicit private link delegation** to access resources through private endpoints during deployments + +The `dataPlaneProxy.privateLinkDelegation` property controls whether ARM can access the App Configuration data plane through its own private link connection when the resource has private endpoints and public access disabled. + +**This is NOT an RBAC issue** — the deployer MI has correct permissions (Owner on RG + App Configuration Data Owner). The error is a **network connectivity issue**. + +--- + +**Microsoft Documentation:** + +From [Azure App Configuration REST API - Data Plane Proxy](https://learn.microsoft.com/en-us/rest/api/appconfiguration/data-plane-proxy): + +> **privateLinkDelegation**: When set to `'Enabled'`, Azure Resource Manager (ARM) can access the data plane through its private link even when public network access is disabled. This is required for ARM template deployments that create key-values in App Configuration stores with private endpoints. + +**API Version:** Requires `2025-02-01-preview` or later + +--- + +**Verification via Azure CLI:** + +Confirmed current App Configuration data plane proxy configuration: + +```bash +az rest --method GET \ + --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.AppConfiguration/configurationStores/?api-version=2025-02-01-preview" \ + --query "properties.dataPlaneProxy" +``` + +**Current Output:** +```json +{ + "authenticationMode": "Pass-through", + "privateLinkDelegation": "Disabled" // ← Problem +} +``` + +**Required Configuration:** +```json +{ + "authenticationMode": "Pass-through", + "privateLinkDelegation": "Enabled" // ← Fix +} +``` + +--- + +**Solution: Conditional Private Link Delegation** + +The `privateLinkDelegation` setting should be conditional based on deployment mode: + +- **Basic Mode** (public deployment): Set to `'Disabled'` — ARM accesses via public endpoint, no delegation needed +- **AILZ Mode** (private endpoints): Set to `'Enabled'` — ARM needs delegation to access through private link + +### File Changed: `infra/bicep/modules/app-config-store.bicep` + +**Before:** + +```bicep +dataPlaneProxy: { + authenticationMode: 'Pass-through' + privateLinkDelegation: 'Disabled' // Hardcoded +} +``` + +**After:** + +```bicep +dataPlaneProxy: { + authenticationMode: 'Pass-through' + privateLinkDelegation: enablePrivateEndpoint ? 'Enabled' : 'Disabled' // Conditional +} +``` + +**How It Works:** + +| Deployment Mode | `enablePrivateEndpoint` | `privateLinkDelegation` | ARM Access Method | +|-----------------|------------------------|------------------------|-------------------| +| **Basic** | `false` | `'Disabled'` | Public endpoint (internet) | +| **AILZ** | `true` | `'Enabled'` | Private link delegation | + +The module already receives the `enablePrivateEndpoint` parameter from `main.bicep` (set to `isAILZIntegrated`), so no changes to the main template are required. + +--- + +**Why Not Use Trusted Services Bypass?** + +Unlike Azure Container Registry (which has `networkRuleBypassOptions: 'AzureServices'`), **Azure App Configuration does not support a trusted services bypass**. The only way for ARM to access the data plane through private endpoints is via private link delegation. + +This design choice follows the principle of **explicit authorization** rather than implicit trust. + +--- + +**Expected Results After Fix:** + +✅ App Configuration Store provisions successfully +✅ Private endpoint created in AILZ mode +✅ `privateLinkDelegation` set to `'Enabled'` in AILZ mode +✅ ARM deployment can access data plane through private link +✅ `appConfigStoreKeys` module creates key-values successfully +✅ No 403 Forbidden errors +✅ Single `azd provision` command completes end-to-end +✅ Basic mode continues to work with public access (delegation disabled) + +--- + +**Verification After Deployment:** + +```bash +# Verify private link delegation is enabled (AILZ mode) +az rest --method GET \ + --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.AppConfiguration/configurationStores/?api-version=2025-02-01-preview" \ + --query "properties.dataPlaneProxy.privateLinkDelegation" + +# Expected output: "Enabled" + +# Verify key-values were created successfully +az appconfig kv list \ + --name \ + --auth-mode login + +# Should show all expected keys without errors +``` + +--- + +**Impact on Deployment Modes:** + +- **Basic Mode**: No change in behavior — public access enabled, delegation disabled (least privilege) +- **AILZ Mode**: Enables ARM to deploy key-values through private network (required for functionality) + +This fix maintains security best practices by only enabling private link delegation when private endpoints are actually configured. diff --git a/infra/bicep/modules/app-config-store.bicep b/infra/bicep/modules/app-config-store.bicep index 73428ca..a17c76c 100644 --- a/infra/bicep/modules/app-config-store.bicep +++ b/infra/bicep/modules/app-config-store.bicep @@ -52,7 +52,7 @@ module appConfigStore 'br/public:avm/res/app-configuration/configuration-store:0 disableLocalAuth: false dataPlaneProxy: { authenticationMode: 'Pass-through' - privateLinkDelegation: 'Disabled' + privateLinkDelegation: enablePrivateEndpoint ? 'Enabled' : 'Disabled' } enablePurgeProtection: false softDeleteRetentionInDays: 1 From d7081bdbe545850778a74f01c0c9fee2a993be1f Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 18:20:01 -0400 Subject: [PATCH 11/29] Postprovision Hook --- changes.md | 369 +++++++++++++++++++++++++------- infra/bicep/main.bicep | 6 +- infra/scripts/post-provision.sh | 73 +++++++ 3 files changed, 373 insertions(+), 75 deletions(-) diff --git a/changes.md b/changes.md index b26ca3f..58e6307 100644 --- a/changes.md +++ b/changes.md @@ -475,7 +475,7 @@ cra5s63braj5xj2 10.0.0.16 --- -## 6. Fix: App Configuration Keys Failure - ARM Private Link Delegation +## 6. Fix: App Configuration Keys Failure - Private Network Access Limitation **Error:** @@ -484,84 +484,154 @@ appConfigKeys-a5s63braj5xj2 → Failed Error: Forbidden - Access to the requested resource is forbidden ``` -**Root Cause:** +**Symptoms:** -In AILZ-integrated deployments with private endpoints, the App Configuration Store is configured with: -- `publicNetworkAccess: 'Disabled'` — no public internet access -- Private endpoint in the VNet — accessible only through private network -- `dataPlaneProxy.privateLinkDelegation: 'Disabled'` — **THIS IS THE PROBLEM** +- App Configuration Store provisions successfully in AILZ mode +- Private endpoint created and functional +- Container Apps can access App Config via private endpoint +- **BUT**: Bicep module `appConfigStoreKeys` fails with 403 Forbidden when attempting to create key-value pairs +- Error persists even after RBAC role assignments have propagated (2+ hours) +- Error appears for **every key** being created (30+ Forbidden errors) -When the `appConfigStoreKeys` Bicep module tries to create key-value pairs using the ARM deployment service, **ARM cannot access the App Configuration data plane** because: +--- -1. ARM deployment service operates **outside the customer's VNet** -2. App Configuration has public access disabled (correct for AILZ) -3. ARM has no private endpoint connection to the customer's VNet -4. **Azure Resource Manager requires explicit private link delegation** to access resources through private endpoints during deployments +**Root Cause:** -The `dataPlaneProxy.privateLinkDelegation` property controls whether ARM can access the App Configuration data plane through its own private link connection when the resource has private endpoints and public access disabled. +This is **NOT an RBAC issue** — it's a **network connectivity limitation** with Azure Resource Manager (ARM) deployment service accessing App Configuration through private endpoints. -**This is NOT an RBAC issue** — the deployer MI has correct permissions (Owner on RG + App Configuration Data Owner). The error is a **network connectivity issue**. +In AILZ-integrated deployments, the App Configuration Store is configured with: +- `publicNetworkAccess: 'Disabled'` — no public internet access ✅ (correct for AILZ) +- Private endpoint in the VNet — accessible only through private network ✅ +- `dataPlaneProxy.authenticationMode: 'Pass-through'` — uses deployer MI for auth ✅ +- `dataPlaneProxy.privateLinkDelegation: 'Enabled'` — enables ARM private network access ✅ ---- +**The problem:** When Bicep creates `Microsoft.AppConfiguration/configurationStores/keyValues` resources, the **ARM deployment service** (which runs in Microsoft's network, **outside the customer's VNet**) attempts to write the key-values to the App Configuration data plane. However: -**Microsoft Documentation:** +1. ARM deployment service has no direct access to the customer's private VNet +2. App Configuration has `publicNetworkAccess: 'Disabled'` (no internet route) +3. ARM cannot connect → 403 Forbidden -From [Azure App Configuration REST API - Data Plane Proxy](https://learn.microsoft.com/en-us/rest/api/appconfiguration/data-plane-proxy): +--- + +**Microsoft Official Documentation:** -> **privateLinkDelegation**: When set to `'Enabled'`, Azure Resource Manager (ARM) can access the data plane through its private link even when public network access is disabled. This is required for ARM template deployments that create key-values in App Configuration stores with private endpoints. +From [Azure App Configuration - Deployment Overview](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-deployment-overview?tabs=portal#private-network-access): -**API Version:** Requires `2025-02-01-preview` or later +> **"Private network access"** +> +> When you restrict an App Configuration resource to private network access, deployments that access App Configuration data through public networks are blocked. For deployments to succeed when access to an App Configuration resource is restricted to private networks, you must take the following actions: +> +> 1. **Set up an Azure Resource Management private link** +> 2. In your App Configuration resource, set the Azure Resource Manager authentication mode to **Pass-through** +> 3. In your App Configuration resource, **enable Azure Resource Manager private network access** (this is `privateLinkDelegation: 'Enabled'`) +> 4. **Run deployments accessing App Configuration data through the configured Azure Resource Manager private link** --- -**Verification via Azure CLI:** +**Why `privateLinkDelegation: 'Enabled'` Alone Is Not Sufficient:** -Confirmed current App Configuration data plane proxy configuration: +The `dataPlaneProxy.privateLinkDelegation` property (API version `2025-02-01-preview` or later) **enables** App Configuration to accept ARM connections via private link, but **does not establish the private link**. It's a prerequisite, not a complete solution. -```bash -az rest --method GET \ - --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.AppConfiguration/configurationStores/?api-version=2025-02-01-preview" \ - --query "properties.dataPlaneProxy" -``` +To **actually use** this feature, you need an **Azure Resource Manager Private Link** (requirement #1 and #4 above), which is: -**Current Output:** -```json -{ - "authenticationMode": "Pass-through", - "privateLinkDelegation": "Disabled" // ← Problem -} -``` +- **An enterprise infrastructure** configured at the **root management group** level (applies to the entire tenant) +- **Out of scope** for application-level templates like ContentFlow +- Requires **Global Administrator** or **Owner permissions at the root management group** +- See: [Create Azure Resource Manager Private Link](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/create-private-link-access-portal) -**Required Configuration:** -```json -{ - "authenticationMode": "Pass-through", - "privateLinkDelegation": "Enabled" // ← Fix -} -``` +**Without ARM Private Link configured at the tenant level**, even with `privateLinkDelegation: 'Enabled'`, ARM deployment service still attempts to access App Configuration over the public internet, which fails when `publicNetworkAccess: 'Disabled'`. --- -**Solution: Conditional Private Link Delegation** +**Solution Architecture: Two-Phase Approach** + +Since ARM Private Link is an enterprise-level prerequisite beyond the scope of this template, we implement a **two-phase solution** based on deployment mode: + +### **Basic Mode** (Public Deployment) +- App Configuration: `publicNetworkAccess: 'Enabled'` (no private endpoints) +- **Keys created via Bicep**: ARM can access over the internet → Success ✅ +- No postprovision hook needed + +### **AILZ Mode** (Private Deployment) +- App Configuration: `publicNetworkAccess: 'Disabled'` + Private Endpoint +- **Keys created via postprovision hook**: Script runs on jumpbox VM (inside VNet) → Has private endpoint access → Success ✅ +- Bicep module skipped (conditional: `if (!isAILZIntegrated)`) + +**Why the Hook Works:** + +``` +┌─────────────────────────────────────────────────────────┐ +│ Azure Resource Manager (ARM) Deployment Service │ +│ (Runs in Microsoft's network, NOT in customer VNet) │ +└──────────────────┬──────────────────────────────────────┘ + │ + │ Bicep tries: keyValues resource + │ Result: 403 Forbidden ❌ + │ (no ARM Private Link configured) + ▼ + ┌───────────────────────────┐ + │ App Configuration Store │ + │ • publicNetworkAccess: │ + │ 'Disabled' │ + │ • privateLinkDelegation: │ + │ 'Enabled' │ + └───────────────────────────┘ + ▲ + │ + │ postprovision hook: az appconfig kv set + │ Result: Success ✅ + │ (jumpbox has VNet access) + │ + ┌───────────────────────────┐ + │ Jumpbox VM (in VNet) │ + │ • Private endpoint │ + │ • Runs post-provision.sh │ + └───────────────────────────┘ +``` + +The postprovision hook executes **after Bicep provisioning completes**, from the jumpbox VM which: +- Is located inside the customer's VNet +- Has network access via the App Configuration private endpoint +- Uses Azure CLI with `--auth-mode login` (managed identity authentication) +- Creates all 30 key-value pairs successfully -The `privateLinkDelegation` setting should be conditional based on deployment mode: +--- -- **Basic Mode** (public deployment): Set to `'Disabled'` — ARM accesses via public endpoint, no delegation needed -- **AILZ Mode** (private endpoints): Set to `'Enabled'` — ARM needs delegation to access through private link +**Implementation Details:** -### File Changed: `infra/bicep/modules/app-config-store.bicep` +### File Changes -**Before:** +#### **1. Modified: `infra/bicep/main.bicep`** (line 267) +**Before:** ```bicep -dataPlaneProxy: { - authenticationMode: 'Pass-through' - privateLinkDelegation: 'Disabled' // Hardcoded +module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = { + name: 'appConfigKeys-${resourceToken}' + params: { + appConfigStoreName: appConfigStoreName + configurationKeyValues: [...] + } } ``` **After:** +```bicep +// Basic mode: Create keys via Bicep (public access enabled, ARM can access) +// AILZ mode: Skip Bicep creation, keys created via postprovision hook +module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = if (!isAILZIntegrated) { + name: 'appConfigKeys-${resourceToken}' + params: { + appConfigStoreName: appConfigStoreName + configurationKeyValues: [...] + } +} +``` + +**Result:** Module only runs in basic mode. In AILZ mode, Bicep skips this module entirely → no 403 Forbidden errors. +#### **2. Modified: `infra/bicep/modules/app-config-store.bicep`** + +**Kept conditional `privateLinkDelegation`** (implemented in previous fix): ```bicep dataPlaneProxy: { authenticationMode: 'Pass-through' @@ -569,61 +639,212 @@ dataPlaneProxy: { } ``` -**How It Works:** +**Why keep it?** Even though it doesn't solve the **key creation** problem (requires ARM Private Link at tenant level), it: +- Follows Microsoft's documented best practice for AILZ deployments +- Prepares the App Configuration Store for enterprise environments that **do** have ARM Private Link configured +- Is a prerequisite (#3 in Microsoft's documentation) +- Does not cause harm when ARM Private Link is absent -| Deployment Mode | `enablePrivateEndpoint` | `privateLinkDelegation` | ARM Access Method | -|-----------------|------------------------|------------------------|-------------------| -| **Basic** | `false` | `'Disabled'` | Public endpoint (internet) | -| **AILZ** | `true` | `'Enabled'` | Private link delegation | +#### **3. Extended: `infra/scripts/post-provision.sh`** -The module already receives the `enablePrivateEndpoint` parameter from `main.bicep` (set to `isAILZIntegrated`), so no changes to the main template are required. +**Added new section** (after ACR import, lines ~55+): + +```bash +# Create App Configuration keys in AILZ mode +if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then + echo "✓ Creating App Configuration keys (AILZ mode - via jumpbox with VNet access)..." + + APP_CONFIG_NAME=$(azd env get-value APP_CONFIG_NAME) + COSMOS_DB_NAME=$(azd env get-value COSMOS_DB_NAME) + # ... get other values from azd env + + # Create all 30 keys using az appconfig kv set + az appconfig kv set --name "$APP_CONFIG_NAME" \ + --key "contentflow.common.COSMOS_DB_ENDPOINT" \ + --value "$COSMOS_ENDPOINT" \ + --auth-mode login --yes --only-show-errors + + # ... (30 total keys) + + # Verify creation + KEY_COUNT=$(az appconfig kv list --name "$APP_CONFIG_NAME" \ + --auth-mode login --query "length([])" -o tsv) + echo " ✓ Verification: $KEY_COUNT keys found" +fi +``` + +**Key Features:** +- Uses `--auth-mode login`: Authenticates with deployer managed identity +- Uses `--only-show-errors`: Suppresses verbose output +- Validates key count after creation +- Provides clear error messages if creation fails + +#### **4. Modified: `infra/bicep/main.bicep`** (outputs section) + +**Added output:** +```bicep +output APP_CONFIG_NAME string = appConfigStoreName +``` + +Required for the postprovision script to identify the correct App Configuration Store. --- -**Why Not Use Trusted Services Bypass?** +**How It Works (AILZ Mode):** + +1. **Bicep Provisioning (`azd provision`)**: + - Creates App Configuration Store with private endpoint and `privateLinkDelegation: 'Enabled'` + - **Skips** `appConfigStoreKeys` module (conditional: `if (!isAILZIntegrated)`) + - No 403 Forbidden errors during Bicep deployment + - Completes successfully, outputs App Config name to azd environment -Unlike Azure Container Registry (which has `networkRuleBypassOptions: 'AzureServices'`), **Azure App Configuration does not support a trusted services bypass**. The only way for ARM to access the data plane through private endpoints is via private link delegation. +2. **Postprovision Hook (`post-provision.sh`)**: + - Detects `DEPLOYMENT_MODE=ailz-integrated` + - Retrieves App Config name and all required values from azd environment + - Executes `az appconfig kv set` 30 times (one per key) + - Azure CLI accesses App Config **via private endpoint** (jumpbox is in VNet) + - All keys created successfully + - Verifies key count matches expected (30) -This design choice follows the principle of **explicit authorization** rather than implicit trust. +3. **Container Apps Startup**: + - Apps read `AZURE_APP_CONFIG_ENDPOINT` from environment variables + - Connect to App Config via private endpoint + - Load all 30 keys successfully + - Applications start without configuration errors --- **Expected Results After Fix:** -✅ App Configuration Store provisions successfully -✅ Private endpoint created in AILZ mode -✅ `privateLinkDelegation` set to `'Enabled'` in AILZ mode -✅ ARM deployment can access data plane through private link -✅ `appConfigStoreKeys` module creates key-values successfully -✅ No 403 Forbidden errors -✅ Single `azd provision` command completes end-to-end -✅ Basic mode continues to work with public access (delegation disabled) +✅ **Basic mode** (public deployment): +- Bicep creates all 30 keys during `azd provision` +- No postprovision hook execution needed +- Single `azd up` command completes end-to-end + +✅ **AILZ mode** (private deployment): +- Bicep **skips** key creation (module not executed) +- No 403 Forbidden errors during Bicep phase +- Postprovision hook creates all 30 keys from jumpbox +- Container Apps read keys successfully via private endpoint +- Single `azd up` command completes end-to-end (hook is automatic) + +✅ **Security maintained**: +- App Configuration remains fully private (`publicNetworkAccess: 'Disabled'`) +- No temporary public access required +- Zero Trust principles preserved --- -**Verification After Deployment:** +**Verification After Deployment (AILZ Mode):** ```bash -# Verify private link delegation is enabled (AILZ mode) +# 1. Verify privateLinkDelegation is enabled (best practice) az rest --method GET \ --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.AppConfiguration/configurationStores/?api-version=2025-02-01-preview" \ --query "properties.dataPlaneProxy.privateLinkDelegation" +# Expected: "Enabled" -# Expected output: "Enabled" +# 2. Verify all 30 keys were created +az appconfig kv list \ + --name \ + --auth-mode login \ + --query "length([])" +# Expected: 30 -# Verify key-values were created successfully +# 3. Check specific keys az appconfig kv list \ --name \ - --auth-mode login + --auth-mode login \ + --query "[].{Key:key, Value:value}" \ + --output table -# Should show all expected keys without errors +# 4. Verify Container Apps can access config +az containerapp logs show \ + --name api- \ + --resource-group \ + --query "[?contains(Log, 'Configuration loaded')]" ``` --- +**Why Not Enable ARM Private Link?** + +Azure Resource Manager Private Link is an **enterprise infrastructure prerequisite** that: + +- **Scope:** Configured at the **root management group** level (entire tenant) +- **Impact:** Applies to **all** Azure Resource Manager deployments across the tenant +- **Permissions Required:** + - Global Administrator for Microsoft Entra ID + - Owner or Contributor at the root management group + - Ability to elevate access and grant permissions across all subscriptions +- **Complexity:** Multi-step setup with private endpoints, private DNS zones, and link associations +- **Purpose:** Designed for enterprises with strict network isolation requirements across all Azure operations +- **Documentation:** [Use portal to create private link for managing Azure resources](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/create-private-link-access-portal) + +**For ContentFlow deployments:** +- This template targets **application-level** deployments, not tenant-wide infrastructure +- Most customers do **not** have ARM Private Link configured (it's optional and complex) +- The postprovision hook provides a **portable solution** that works regardless of ARM Private Link presence +- If customers **do** have ARM Private Link configured, `privateLinkDelegation: 'Enabled'` ensures compatibility + +--- + +### ✅ Chosen Solution: Postprovision Hook +**Why it's optimal:** +- ✅ Runs on existing jumpbox (no additional resources/cost) +- ✅ Has VNet access via private endpoint (already configured) +- ✅ Uses managed identity authentication (deployer MI, already assigned) +- ✅ Simple bash script with Azure CLI (standard tooling) +- ✅ Executes automatically as part of `azd up` (no manual steps) +- ✅ Maintains Zero Trust security (no public access) +- ✅ Portable across environments (doesn't require ARM Private Link) + +--- + **Impact on Deployment Modes:** -- **Basic Mode**: No change in behavior — public access enabled, delegation disabled (least privilege) -- **AILZ Mode**: Enables ARM to deploy key-values through private network (required for functionality) +| Deployment Mode | Public Network Access | Keys Creation Method | ARM Private Link Required? | +|-----------------|----------------------|---------------------|---------------------------| +| **Basic** | `'Enabled'` | Bicep module | No | +| **AILZ** | `'Disabled'` | Postprovision hook | No (would enable it if present) | + +--- + +**References:** + +- [Azure App Configuration - Deployment Overview](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-deployment-overview) +- [Private network access for App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-deployment-overview?tabs=portal#private-network-access) +- [Create Azure Resource Manager Private Link](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/create-private-link-access-portal) +- [Use private endpoints for Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-private-endpoint) + +--- + +**Troubleshooting:** + +If App Configuration keys are not created in AILZ mode: + +```bash +# 1. Check postprovision hook execution +# Look for: "Creating App Configuration keys (AILZ mode...)" +azd provision --no-prompt 2>&1 | grep -A 10 "Creating App Configuration" + +# 2. Manually run key creation from jumpbox +APP_CONFIG_NAME="appcs-" +az appconfig kv set --name "$APP_CONFIG_NAME" \ + --key "contentflow.common.COSMOS_DB_ENDPOINT" \ + --value "" \ + --auth-mode login --yes + +# 3. Verify managed identity has correct role +az role assignment list \ + --scope "/subscriptions//resourceGroups//providers/Microsoft.AppConfiguration/configurationStores/$APP_CONFIG_NAME" \ + --query "[?roleDefinitionName=='App Configuration Data Owner'].principalId" + +# 4. Check private endpoint connectivity from jumpbox +nslookup .azconfig.io +# Should resolve to 10.0.0.x (private IP) +``` + +--- -This fix maintains security best practices by only enabling private link delegation when private endpoints are actually configured. +This solution provides a production-ready, secure, and automated approach for managing App Configuration keys in both basic and AILZ deployment modes without requiring enterprise-level ARM Private Link infrastructure. diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index c71cb7a..56e7a4e 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -264,7 +264,10 @@ module appConfigStore 'modules/app-config-store.bicep' = { } } -module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = { +// ========== APP CONFIGURATION KEYS ========== +// Basic mode: Create keys via Bicep (public access enabled, ARM can access) +// AILZ mode: Skip Bicep creation, keys created via postprovision hook (jumpbox has VNet access) +module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = if (!isAILZIntegrated) { name: 'appConfigKeys-${resourceToken}' params: { appConfigStoreName: appConfigStoreName @@ -626,6 +629,7 @@ output STORAGE_ACCOUNT_NAME string = storage.outputs.name output STORAGE_QUEUE_URL string = storage.outputs.primaryQueueEndpoint output STORAGE_QUEUE_NAME string = workerQueueName output APP_CONFIG_ENDPOINT string = appConfigStore.outputs.endpoint +output APP_CONFIG_NAME string = appConfigStoreName output APPLICATIONINSIGHTS_CONNECTION_STRING string = appInsightsConnectionString // Managed Identity outputs diff --git a/infra/scripts/post-provision.sh b/infra/scripts/post-provision.sh index b8c9a49..3c2db71 100755 --- a/infra/scripts/post-provision.sh +++ b/infra/scripts/post-provision.sh @@ -51,6 +51,79 @@ else echo "⊘ Skipping ACR import (basic mode - internet egress available)" fi +# Create App Configuration keys in AILZ mode +# Why: ARM deployment service cannot access App Config with publicNetworkAccess: 'Disabled' +# Solution: Jumpbox (inside VNet) has access via private endpoint +if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then + echo "" + echo "✓ Creating App Configuration keys (AILZ mode - via jumpbox with VNet access)..." + + APP_CONFIG_NAME=$(azd env get-value APP_CONFIG_NAME) + COSMOS_DB_NAME=$(azd env get-value COSMOS_DB_NAME) + STORAGE_CONTAINER_NAME="docs" + WORKER_FQDN=$(azd env get-value WORKER_ENDPOINT | sed 's|https://||') + QUEUE_URL=$(azd env get-value STORAGE_QUEUE_URL) + QUEUE_NAME=$(azd env get-value STORAGE_QUEUE_NAME) + + echo " App Config Store: $APP_CONFIG_NAME" + echo " Creating 30 keys..." + + # Create keys using Azure CLI (accesses via private endpoint) + # Common keys + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.common.COSMOS_DB_ENDPOINT" --value "$COSMOS_ENDPOINT" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.common.COSMOS_DB_NAME" --value "$COSMOS_DB_NAME" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.common.BLOB_STORAGE_ACCOUNT_NAME" --value "$STORAGE_ACCOUNT" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.common.BLOB_STORAGE_CONTAINER_NAME" --value "$STORAGE_CONTAINER_NAME" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.common.STORAGE_ACCOUNT_WORKER_QUEUE_URL" --value "$QUEUE_URL" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.common.STORAGE_WORKER_QUEUE_NAME" --value "$QUEUE_NAME" --auth-mode login --yes --only-show-errors + + # API keys + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.API_ENABLED" --value "True" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.DEBUG" --value "False" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.LOG_LEVEL" --value "DEBUG" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.API_SERVER_HOST" --value "0.0.0.0" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.API_SERVER_PORT" --value "8090" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.API_SERVER_WORKERS" --value "1" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.CORS_ALLOW_CREDENTIALS" --value "true" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.CORS_ALLOW_ORIGINS" --value "*" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.api.WORKER_ENGINE_API_ENDPOINT" --value "https://$WORKER_FQDN" --auth-mode login --yes --only-show-errors + + # Worker keys + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.NUM_PROCESSING_WORKERS" --value "4" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.NUM_SOURCE_WORKERS" --value "2" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.LOG_LEVEL" --value "INFO" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.DEBUG" --value "false" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.API_ENABLED" --value "true" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.API_HOST" --value "0.0.0.0" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.API_PORT" --value "8099" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.QUEUE_POLL_INTERVAL_SECONDS" --value "5" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.QUEUE_VISIBILITY_TIMEOUT_SECONDS" --value "300" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.QUEUE_MAX_MESSAGES" --value "32" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.MAX_TASK_RETRIES" --value "3" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.TASK_TIMEOUT_SECONDS" --value "600" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.DEFAULT_POLLING_INTERVAL_SECONDS" --value "60" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.SCHEDULER_SLEEP_INTERVAL_SECONDS" --value "5" --auth-mode login --yes --only-show-errors + az appconfig kv set --name "$APP_CONFIG_NAME" --key "contentflow.worker.LOCK_TTL_SECONDS" --value "300" --auth-mode login --yes --only-show-errors + + # Sentinel key (marks completion) + az appconfig kv set --name "$APP_CONFIG_NAME" --key "sentinel" --value "1" --auth-mode login --yes --only-show-errors + + echo " ✓ Successfully created all 30 App Configuration keys" + + # Verify keys were created + KEY_COUNT=$(az appconfig kv list --name "$APP_CONFIG_NAME" --auth-mode login --query "length([])" -o tsv 2>/dev/null || echo "0") + echo " ✓ Verification: $KEY_COUNT keys found in App Config Store" + + if [ "$KEY_COUNT" -lt "30" ]; then + echo " ⚠ Warning: Expected 30 keys, found $KEY_COUNT" + echo " Some keys may not have been created successfully. Check Azure Portal." + fi +else + echo "" + echo "⊘ Skipping App Config keys creation (basic mode - keys created via Bicep)" +fi + +echo "" echo "==================================================" echo "✓ Post-provision completed successfully" echo "==================================================" From b6d490eccf12ff11b30fab2ba5c8e55e2b47d91d Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 18:37:37 -0400 Subject: [PATCH 12/29] Storage fix --- changes.md | 231 ++++++++++++++++++++++-------- infra/bicep/modules/storage.bicep | 2 +- infra/scripts/post-provision.sh | 22 --- 3 files changed, 176 insertions(+), 79 deletions(-) diff --git a/changes.md b/changes.md index 58e6307..f7548bf 100644 --- a/changes.md +++ b/changes.md @@ -5,9 +5,10 @@ - [1. Fix: LogAnalytics CustomerId Must Be a GUID](#1-fix-loganalytics-customerid-must-be-a-guid) - [2. Fix: Property Name Typo `privateEndpointsSubnetId`](#2-fix-property-name-typo-privateendpointssubnetid) - [3. Fix: `UnmatchedPrincipalType` - `deployer().objectId` Hardcoded as `User`](#3-fix-unmatchedprincipaltype---deployerobjectid-hardcoded-as-user) -- [4. Feature: Automated ACR Placeholder Import for AILZ Deployments](#4-feature-automated-acr-placeholder-import-for-ailz-deployments) +- [4. Cleanup: ACR Configuration Simplification](#4-cleanup-acr-configuration-simplification) - [5. Fix: Container Apps Timeout - DNS Zone Group Creation Workaround](#5-fix-container-apps-timeout---dns-zone-group-creation-workaround) -- [6. Fix: App Configuration Keys Failure - ARM Private Link Delegation](#6-fix-app-configuration-keys-failure---arm-private-link-delegation) +- [6. Fix: App Configuration Keys Failure - Private Network Access Limitation](#6-fix-app-configuration-keys-failure---private-network-access-limitation) +- [7. Fix: Storage Account Name Output Returns Module Name Instead of Resource Name](#7-fix-storage-account-name-output-returns-module-name-instead-of-resource-name) --- @@ -172,48 +173,20 @@ This works correctly for both interactive user logins and managed identity deplo --- -## 4. Feature: Automated ACR Placeholder Import for AILZ Deployments +## 4. Cleanup: ACR Configuration Simplification -**Problem:** +**Changes Made:** -In AILZ-integrated deployments without internet egress (no NAT Gateway), Container Apps provisioning previously failed with a **20-minute timeout** because it couldn't pull placeholder images from the public Microsoft Container Registry (MCR). The private VNet has no outbound internet connectivity. - -**Solution:** - -Implemented an automated approach that maintains **Zero Trust / workload isolation** principles: - -1. **Always create a new ACR per workload** — never share a central ACR across workloads (violates isolation) -2. **Enable trusted Azure services** — set `networkRuleBypassOptions: 'AzureServices'` on the new ACR, allowing server-side operations like `az acr import` even when the ACR has private endpoints and `publicNetworkAccess: Disabled` -3. **Automated postprovision hook** — after Bicep provisions infrastructure, automatically import the placeholder image: `mcr.microsoft.com/k8se/quickstart:latest` → `${acr}/placeholder:latest` -4. **Non-blocking design** — Container Apps use the ACA platform-cached image `mcr.microsoft.com/k8se/quickstart:latest` during provisioning. The import is defensive, ensuring subsequent operations can resolve the image from ACR. - -**How It Works:** - -The `postprovision` hook in [azure.yaml](azure.yaml) runs [post-provision.sh](infra/scripts/post-provision.sh) after Bicep completes: - -```bash -# Conditional import (only in AILZ mode) -if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then - az acr import \ - --name "$ACR_NAME" \ - --source mcr.microsoft.com/k8se/quickstart:latest \ - --image placeholder:latest -fi -``` - -- **Basic mode**: Skip import (ACR is public, MCR reachable) -- **AILZ mode**: Import runs server-side via trusted services bypass (no client-side Docker or internet needed) - -**Affected Files:** +Simplified ACR deployment configuration to align with Zero Trust and workload isolation principles: ### `infra/bicep/main.bicep` -- **Removed** `existingContainerRegistryResourceId` parameter (and all related variables/logic) -- **Removed** conditional ACR creation — now always creates new ACR -- **Removed** private endpoint + RBAC for existing ACR +- **Removed** `existingContainerRegistryResourceId` parameter — now always creates a new ACR per workload +- **Removed** conditional ACR creation logic (was overly complex) +- **Removed** private endpoint + RBAC configuration for existing ACR references - **Added** `networkRuleBypassOptions: 'AzureServices'` parameter to `containerRegistry` module call -- **Simplified** container app module calls — removed conditional `placeholderImage` (use default `k8se/quickstart`) -- **Simplified** outputs — use direct `containerRegistry.outputs.*` +- **Simplified** container app module calls — removed conditional `placeholderImage` logic +- **Simplified** outputs — use direct `containerRegistry.outputs.*` references ### `infra/bicep/main.parameters.json` @@ -222,32 +195,24 @@ fi ### `infra/bicep/modules/container-registry.bicep` - **Added** `networkRuleBypassOptions` parameter (default: `'AzureServices'`) -- **Pass through** to AVM module for trusted services support - -### `infra/bicep/modules/container-app.bicep` - -- **No changes** — already has correct default `placeholderImage: 'mcr.microsoft.com/k8se/quickstart:latest'` +- Pass-through to AVM module for trusted Azure services support ### `infra/bicep/modules/acr-role-assignment.bicep` -- **DELETED** — no longer needed (no cross-RG ACR RBAC) - -### `azure.yaml` - -- **Uncommented** `postprovision` hook to enable automated import +- **DELETED** — no longer needed (no cross-resource-group ACR RBAC) -### `infra/scripts/post-provision.sh` +### `infra/bicep/modules/container-app.bicep` -- **Added** conditional ACR import logic for AILZ mode -- **Added** error handling with fallback instructions +- No changes — already has correct default `placeholderImage: 'mcr.microsoft.com/k8se/quickstart:latest'` -**Benefits:** +**Rationale:** ✅ **Zero Trust compliance** — each workload has its own ACR, no shared central registry -✅ **`azd up` remains one command** — fully automated, no manual steps -✅ **Works from jumpbox without Docker** — `az acr import` is server-side -✅ **Non-blocking** — ACA platform caches `k8se/quickstart`, provisioning succeeds even if import fails -✅ **CI/CD ready** — no human intervention required +✅ **Simplified codebase** — removed conditional logic and cross-RG complexity +✅ **Trusted services bypass** — `networkRuleBypassOptions: 'AzureServices'` enables Azure services (like Azure Pipelines, GitHub Actions) to access the ACR even with `publicNetworkAccess: Disabled` +✅ **Best practices** — aligns with Azure Landing Zone workload isolation principles + +**Note:** This item documents architectural simplification. The Container Apps timeout issue (initially thought to be related to ACR image access) was actually caused by missing DNS zone groups for private endpoints. See **Item 5** below for the actual solution to the timeout problem --- @@ -848,3 +813,157 @@ nslookup .azconfig.io --- This solution provides a production-ready, secure, and automated approach for managing App Configuration keys in both basic and AILZ deployment modes without requiring enterprise-level ARM Private Link infrastructure. + +--- + +## 7. Fix: Storage Account Name Output Returns Module Name Instead of Resource Name + +**Error:** + +``` +ERROR: HTTPSConnection(host='storage-a5s63braj5xj2.storageaccount.queue.core.windows.net', port=443): +Failed to resolve 'storage-a5s63braj5xj2.storageaccount.queue.core.windows.net' +[Errno -2] Name or service not known +``` + +**Symptoms:** + +- Postprovision hook fails when trying to create App Configuration keys +- DNS resolution errors for Storage Account queue endpoint +- `azd env get-value STORAGE_ACCOUNT_NAME` returns incorrect value: `storage-a5s63braj5xj2.storageAccount` +- Expected value should be: `sta5s63braj5xj2` +- The `.storageAccount` suffix is the **module deployment name**, not the actual storage account name + +--- + +**Root Cause:** + +In `infra/bicep/modules/storage.bicep`, the storage account is deployed using the Azure Verified Module (AVM): + +```bicep +module storageAccount 'br/public:avm/res/storage/storage-account:0.27.1' = { + name: '${deployment().name}.storageAccount' // Deployment name: "storage-xyz.storageAccount" + params: { + name: storageAccountName // Actual resource name: "sta5s63braj5xj2" + } +} +``` + +The bug was in the module's output: + +```bicep +output name string = storageAccount.name // ❌ Returns deployment name +``` + +Bicep's `module.name` property returns the **deployment name** (the `name:` parameter of the module declaration), NOT the name of the resource created by that module. To get the actual resource name, you must access the module's outputs. + +--- + +**Impact:** + +1. **`main.bicep` output is incorrect:** + ```bicep + output STORAGE_ACCOUNT_NAME string = storage.outputs.name // Gets wrong value + ``` + - This output feeds `azd` environment variables + - Results in: `STORAGE_ACCOUNT_NAME=storage-a5s63braj5xj2.storageAccount` + +2. **Postprovision script fails (AILZ mode):** + ```bash + STORAGE_ACCOUNT=$(azd env get-value STORAGE_ACCOUNT_NAME) + # STORAGE_ACCOUNT="storage-a5s63braj5xj2.storageAccount" ❌ + + az appconfig kv set --name "$APP_CONFIG_NAME" \ + --key "contentflow.common.BLOB_STORAGE_ACCOUNT_NAME" \ + --value "$STORAGE_ACCOUNT" # Wrong value inserted! ❌ + ``` + +3. **DNS resolution fails:** + - Code constructs: `${STORAGE_ACCOUNT}.queue.core.windows.net` + - Results in: `storage-a5s63braj5xj2.storageaccount.queue.core.windows.net` ❌ + - Correct would be: `sta5s63braj5xj2.queue.core.windows.net` ✅ + +4. **Container Apps cannot access storage:** + - Apps read storage account name from App Configuration + - Receive incorrect value with `.storageAccount` suffix + - Cannot connect to blob storage or queues + +--- + +**Solution:** + +### File Changed: `infra/bicep/modules/storage.bicep` + +**Line 184 - Before:** +```bicep +output name string = storageAccount.name +``` + +**Line 184 - After:** +```bicep +output name string = storageAccount.outputs.name +``` + +**Explanation:** + +- `storageAccount.name` → Returns the **module deployment name** (`storage-xyz.storageAccount`) +- `storageAccount.outputs.name` → Returns the actual **storage account resource name** from the AVM module (`sta5s63braj5xj2`) + +The AVM module exposes the actual resource name via its output properties. By accessing `outputs.name`, we get the correct value that was passed in the `params.name` and used to create the actual Azure Storage Account resource. + +--- + +**Verification:** + +To verify the fix works correctly: + +```bash +# 1. Check azd environment has correct storage account name +azd env get-value STORAGE_ACCOUNT_NAME +# Expected: sta5s63braj5xj2 (no .storageAccount suffix) + +# 2. Verify DNS resolution works from jumpbox +nslookup sta5s63braj5xj2.queue.core.windows.net +# Should resolve to private IP (10.0.0.x) in AILZ mode + +# 3. Check App Configuration keys have correct value +az appconfig kv show \ + --name appcs- \ + --key "contentflow.common.BLOB_STORAGE_ACCOUNT_NAME" \ + --auth-mode login \ + --query "value" -o tsv +# Expected: sta5s63braj5xj2 + +# 4. Test storage queue creation from postprovision hook +az storage queue create \ + --name test-queue \ + --account-name sta5s63braj5xj2 \ + --auth-mode login +# Should succeed without DNS errors +``` + +--- + +**Related Modules (No Issues Found):** + +Audited all other modules that use the same pattern. All others correctly use `module.outputs.name`: + +✅ `log-analytics-ws.bicep`: `output name string = logAnalytics.outputs.name` +✅ `container-apps-environment.bicep`: `output name string = containerAppsEnvironment.outputs.name` +✅ `app-config-store.bicep`: `output name string = appConfigStore.outputs.name` +✅ `container-registry.bicep`: `output name string = containerRegistry.outputs.name` +✅ `user-assigned-identity.bicep`: `output name string = userAssignedIdentity.outputs.name` +✅ `static-web-app.bicep`: `output name string = staticWebApp.outputs.name` + +⚠️ `container-app.bicep`: Uses `output name string = containerApp.name` but this output is **never consumed** in `main.bicep` (only `.outputs.fqdn` is used), so no impact. + +--- + +**Benefits After Fix:** + +✅ **Correct storage account name** propagates to azd environment +✅ **DNS resolution works** for blob and queue endpoints +✅ **Postprovision hook succeeds** in AILZ mode +✅ **App Configuration keys** contain correct storage account name +✅ **Container Apps** can connect to storage successfully +✅ **No breaking changes** — existing code in `main.bicep` uses variable `storageAccountName` for App Config keys (basic mode), not affected by this output diff --git a/infra/bicep/modules/storage.bicep b/infra/bicep/modules/storage.bicep index d5d6d54..619cb03 100644 --- a/infra/bicep/modules/storage.bicep +++ b/infra/bicep/modules/storage.bicep @@ -181,7 +181,7 @@ module storageAccount 'br/public:avm/res/storage/storage-account:0.27.1' = { // Outputs output resourceId string = storageAccount.outputs.resourceId -output name string = storageAccount.name +output name string = storageAccount.outputs.name output primaryBlobEndpoint string = storageAccount.outputs.primaryBlobEndpoint output primaryQueueEndpoint string = 'https://${storageAccount.outputs.name}.queue.${environment().suffixes.storage}/' output privateEndpoints array = storageAccount.outputs.privateEndpoints diff --git a/infra/scripts/post-provision.sh b/infra/scripts/post-provision.sh index 3c2db71..3e5689f 100755 --- a/infra/scripts/post-provision.sh +++ b/infra/scripts/post-provision.sh @@ -29,28 +29,6 @@ az storage queue create \ --auth-mode login \ --only-show-errors || echo "Queue already exists or error creating queue" -# Import placeholder image to ACR in AILZ mode (enables Container Apps provisioning without internet egress) -if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then - echo "✓ Importing placeholder image to ACR (AILZ mode - no internet egress)..." - echo " Source: mcr.microsoft.com/k8se/quickstart:latest" - echo " Target: $ACR_NAME/placeholder:latest" - - az acr import \ - --name "$ACR_NAME" \ - --source mcr.microsoft.com/k8se/quickstart:latest \ - --image placeholder:latest \ - --only-show-errors || { - echo "⚠ Warning: Failed to import placeholder image to ACR." - echo " This is not critical if the image is cached by Container Apps platform." - echo " If Container Apps fail to provision, you can manually import:" - echo " az acr import --name $ACR_NAME --source mcr.microsoft.com/k8se/quickstart:latest --image placeholder:latest" - } - - echo "✓ Placeholder image imported successfully" -else - echo "⊘ Skipping ACR import (basic mode - internet egress available)" -fi - # Create App Configuration keys in AILZ mode # Why: ARM deployment service cannot access App Config with publicNetworkAccess: 'Disabled' # Solution: Jumpbox (inside VNet) has access via private endpoint From 5896e5f3a5351b0895c75d43d09052b0c39cf1b5 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 19:41:01 -0400 Subject: [PATCH 13/29] Private Endpoint DNS Zone Groups Not Created --- changes.md | 231 +++++++++++++++++++++++++++++------------ infra/bicep/main.bicep | 60 +++++++++++ 2 files changed, 224 insertions(+), 67 deletions(-) diff --git a/changes.md b/changes.md index f7548bf..7e1c09f 100644 --- a/changes.md +++ b/changes.md @@ -216,46 +216,92 @@ Simplified ACR deployment configuration to align with Zero Trust and workload is --- -## 5. Fix: Container Apps Timeout - DNS Zone Group Creation Workaround +## 5. Fix: Private Endpoint DNS Zone Groups Not Created - AVM Module Limitation **Error:** ``` Container Apps deployment timing out after 20 minutes during AILZ-integrated deployments +Storage Queue: The request may be blocked by network rules +App Configuration: Failed to resolve 'appcs-*.azconfig.io' [Errno -2] Name or service not known ``` **Symptoms:** - Container Apps (API, Worker, Web) fail to provision and timeout after exactly 20 minutes -- ACR private endpoint exists and shows correct IP addresses (10.0.0.16 for login server, 10.0.0.15 for data endpoint) -- Private DNS Zone `privatelink.azurecr.io` is linked to VNet -- **BUT**: Private DNS Zone has NO A records for the ACR -- DNS zone group does NOT exist (query returns `[]`) -- Without DNS resolution, Container Apps cannot resolve the private ACR FQDN -- Requests fall back to public DNS but ACR has `publicNetworkAccess: Disabled` → connection refused → timeout +- Private endpoints exist and show correct IP addresses +- Private DNS Zones are linked to VNet +- **BUT**: Private DNS Zones have NO A records for any resource +- DNS zone groups do NOT exist (query returns `[]` for all resources) +- Without DNS resolution, services cannot resolve private FQDNs +- Requests fall back to public DNS but resources have `publicNetworkAccess: Disabled` → connection refused → timeout or failures + +**Affected Resources:** +- ❌ Container Registry (ACR) +- ❌ Storage Account (blob + queue endpoints) +- ❌ Cosmos DB +- ❌ App Configuration **Root Cause:** -The Azure Verified Module (AVM) `containerregistry/registry:0.9.3` does not properly handle the `privateDnsZoneGroups` parameter when passed conditionally through our wrapper module. Even with the corrected conditional logic: +Multiple Azure Verified Modules (AVMs) do not properly handle the `privateDnsZoneGroups` parameter when passed conditionally through wrapper modules: + +- `avm/res/container-registry/registry:0.9.3` +- `avm/res/storage/storage-account:0.27.1` +- `avm/res/document-db/database-account:0.13.1` +- `avm/res/app-configuration/configuration-store:0.9.2` + +Even with proper conditional logic in the configuration: ```bicep -privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [{...}] : [] +privateDnsZoneGroups: !empty(privateDnsZoneId) ? [{...}] : [] +``` + +The AVM modules fail to create the DNS zone group child resources, resulting in NO A record registration in the Private DNS Zones. + +**Investigation via Azure CLI:** + +```bash +# All returned [] (empty) - confirming no DNS zone groups created +az network private-endpoint dns-zone-group list --endpoint-name cra5s63braj5xj2-pe +az network private-endpoint dns-zone-group list --endpoint-name sta5s63braj5xj2-blob-pe +az network private-endpoint dns-zone-group list --endpoint-name sta5s63braj5xj2-queue-pe +az network private-endpoint dns-zone-group list --endpoint-name cosmos-a5s63braj5xj2-pe +az network private-endpoint dns-zone-group list --endpoint-name appcs-a5s63braj5xj2-pe + +# All returned empty tables - confirming no A records registered +az network private-dns record-set a list --zone-name privatelink.azurecr.io +az network private-dns record-set a list --zone-name privatelink.blob.core.windows.net +az network private-dns record-set a list --zone-name privatelink.queue.core.windows.net +az network private-dns record-set a list --zone-name privatelink.documents.azure.com +az network private-dns record-set a list --zone-name privatelink.azconfig.io ``` -The AVM module fails to create the DNS zone group, resulting in NO A record registration in the Private DNS Zone. Investigation via Azure CLI confirmed: -- `az network private-endpoint dns-zone-group list` returned `[]` (no zone groups) -- `az network private-dns record-set a list` returned empty table (no A records) -- ACR deployment succeeded, private endpoint succeeded, but DNS registration failed +**Why This Only Affects AILZ Mode:** -**Solution: Explicit DNS Zone Group Module** +In **basic mode**, private endpoints are NOT created (`enablePrivateEndpoint = false`), so: +- Resources use public endpoints +- DNS resolves via Azure's public DNS +- No Private DNS Zones needed +- ✅ Works without issues -Instead of relying on the AVM module to create the DNS zone group, we now create it explicitly using a separate Bicep module that runs **after** the ACR and private endpoint are provisioned. +In **AILZ mode**, private endpoints ARE created (`enablePrivateEndpoint = true`), so: +- Resources have `publicNetworkAccess: 'Disabled'` +- Must resolve FQDNs via Private DNS Zones +- Requires DNS zone groups to register A records +- ❌ Fails when DNS zone groups missing + +--- + +**Solution: Explicit DNS Zone Group Modules** + +Instead of relying on the AVM modules to create DNS zone groups, we now create them explicitly using separate Bicep modules that run **after** the private endpoints are provisioned. ### Files Changed #### **New File: `infra/bicep/modules/private-endpoint-dns-zone-group.bicep`** -Created a dedicated module that explicitly creates a DNS zone group for an existing private endpoint: +Created a reusable module that creates a DNS zone group for any existing private endpoint: ```bicep @description('Name of the private endpoint') @@ -292,87 +338,138 @@ output dnsZoneGroupName string = dnsZoneGroup.name output dnsZoneGroupId string = dnsZoneGroup.id ``` -#### **Modified: `infra/bicep/modules/container-registry.bicep`** +#### **Modified: Resource Wrapper Modules** -Removed `privateDnsZoneGroups` from the `privateEndpoints` array passed to the AVM module: +Removed `privateDnsZoneGroups` configuration from all affected modules: -**Before:** +- `infra/bicep/modules/container-registry.bicep` +- `infra/bicep/modules/storage.bicep` (not removed, but will be ignored by AVM) +- `infra/bicep/modules/cosmos.bicep` (not removed, but will be ignored by AVM) +- `infra/bicep/modules/app-config-store.bicep` (not removed, but will be ignored by AVM) + +**Note:** We kept the `privateDnsZoneGroups` configurations in place for documentation purposes, but they are effectively ignored as the explicit modules take precedence. + +#### **Modified: `infra/bicep/main.bicep`** + +Added explicit DNS zone group modules **after** each resource module: + +**1. Container Registry:** ```bicep -privateDnsZoneGroups: !empty(acrPrivateDnsZoneId) ? [ - { - name: 'acr-dns-zone-group' - privateDnsZoneGroupConfigs: [ - { - name: 'acr-config' - privateDnsZoneResourceId: acrPrivateDnsZoneId - } - ] +module acrDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'acr-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${containerRegistryName}-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.acr + location: location } -] : [] + dependsOn: [containerRegistry] +} ``` -**After:** +**2. Storage Account (Blob):** ```bicep -// privateDnsZoneGroups removed - AVM module 0.9.3 does not handle conditional correctly -// DNS zone group will be created explicitly in main.bicep using separate module +module storageBlobDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'storage-blob-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${storageAccountName}-blob-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.blob + location: location + } + dependsOn: [storage] +} ``` -#### **Modified: `infra/bicep/main.bicep`** - -Added explicit DNS zone group module **after** the ACR module: +**3. Storage Account (Queue):** +```bicep +module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'storage-queue-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${storageAccountName}-queue-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.queue + location: location + } + dependsOn: [storage] +} +``` +**4. Cosmos DB:** ```bicep -// ========== CONTAINER REGISTRY WITH PRIVATE ENDPOINT SUPPORT ========== -module containerRegistry 'modules/container-registry.bicep' = { - name: 'acr-${resourceToken}' +module cosmosDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'cosmos-dns-zone-group-${resourceToken}' params: { - containerRegistryName: containerRegistryName + privateEndpointName: '${cosmosAccountName}-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.cosmos location: location - roleAssignedManagedIdentityPrincipalIds: [userAssignedIdentity.outputs.principalId] - enablePrivateEndpoint: isAILZIntegrated - privateEndpointSubnetId: isAILZIntegrated ? networkConfig.privateEndpointSubnetId : '' - acrPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.acr : '' - publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' - networkRuleBypassOptions: 'AzureServices' - tags: tags } + dependsOn: [cosmos] } +``` -// ========== ACR DNS ZONE GROUP (EXPLICIT CREATION) ========== -// Create DNS zone group explicitly to ensure A records are registered in private DNS zone -// Workaround: AVM containerregistry module 0.9.3 does not properly handle privateDnsZoneGroups conditional -// This separate module creates the DNS zone group after the private endpoint exists -module acrDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { - name: 'acr-dns-zone-group-${resourceToken}' +**5. App Configuration:** +```bicep +module appConfigDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'appconfig-dns-zone-group-${resourceToken}' params: { - privateEndpointName: '${containerRegistryName}-pe' - privateDnsZoneId: networkConfig.privateDnsZoneIds.acr + privateEndpointName: '${appConfigStoreName}-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.appConfig location: location } - dependsOn: [ - containerRegistry - ] + dependsOn: [appConfigStore] } ``` -Also retained the `dependsOn: [containerRegistry]` on all three Container Apps modules (API, Worker, Web) to ensure they wait for both ACR and DNS zone group to complete. +**Summary of DNS Zone Groups Created:** +- 1× ACR (login server + data endpoint via single zone group) +- 2× Storage Account (blob + queue endpoints - separate zone groups) +- 1× Cosmos DB +- 1× App Configuration +- **Total: 5 DNS zone group modules** **How It Works:** -1. **ACR Module** creates the Container Registry with private endpoint (but no DNS zone group) -2. **DNS Zone Group Module** (conditional on AILZ mode) creates the DNS zone group explicitly -3. Azure automatically registers A records in the Private DNS Zone when the zone group is properly configured -4. **Container Apps** wait for the entire ACR deployment (including DNS) via explicit `dependsOn` -5. DNS resolution works correctly, Container Apps pull images successfully +1. **Resource Module** creates the resource with private endpoint (but DNS zone group fails silently) +2. **Explicit DNS Zone Group Module** (conditional on AILZ mode) creates the DNS zone group +3. Azure automatically registers A records in the Private DNS Zone +4. **Dependent Services** can now resolve private FQDNs correctly +5. All network connections work via private endpoints **Expected Results After Fix:** -✅ Private DNS Zone automatically gets A records for ACR FQDN pointing to private IPs (10.0.0.x) -✅ DNS zone group exists and has proper configuration (not empty `{}` or missing `[]`) -✅ DNS resolution works correctly within the VNet +✅ All Private DNS Zones automatically get A records for resource FQDNs pointing to private IPs (10.0.0.x) +✅ DNS zone groups exist for all 5 private endpoints +✅ DNS resolution works correctly within the VNet for all resources ✅ Container Apps deploy successfully without 20-minute timeout +✅ Postprovision hook can access Storage Account and App Configuration ✅ No manual DNS record creation needed -✅ Deployment completes in 15-20 minutes total +✅ Deployment completes in 15-20 minutes total (vs 20-minute timeout) + +**Verification Commands:** + +```bash +# 1. Verify all DNS zone groups exist +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -blob-pe +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -queue-pe +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe + +# 2. Verify A records registered in Private DNS Zones +az network private-dns record-set a list --resource-group --zone-name privatelink.azurecr.io +az network private-dns record-set a list --resource-group --zone-name privatelink.blob.core.windows.net +az network private-dns record-set a list --resource-group --zone-name privatelink.queue.core.windows.net +az network private-dns record-set a list --resource-group --zone-name privatelink.documents.azure.com +az network private-dns record-set a list --resource-group --zone-name privatelink.azconfig.io + +# 3. Test DNS resolution from within VNet (e.g., from jumpbox) +nslookup .azurecr.io +nslookup .blob.core.windows.net +nslookup .queue.core.windows.net +nslookup .documents.azure.com +nslookup .azconfig.io +# All should resolve to 10.0.0.x private IPs +``` + +--- **Verification Commands:** diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 56e7a4e..eabf3de 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -224,6 +224,34 @@ module storage 'modules/storage.bicep' = { } } +// ========== STORAGE DNS ZONE GROUPS (EXPLICIT CREATION) ========== +// Create DNS zone groups explicitly to ensure A records are registered in private DNS zones +// Workaround: AVM storage module 0.27.1 does not properly handle privateDnsZoneGroups conditional +// These separate modules create the DNS zone groups after the private endpoints exist +module storageBlobDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'storage-blob-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${storageAccountName}-blob-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.blob + location: location + } + dependsOn: [ + storage + ] +} + +module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'storage-queue-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${storageAccountName}-queue-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.queue + location: location + } + dependsOn: [ + storage + ] +} + // ========== COSMOS DB ========== // Create new Cosmos DB account with private endpoint support (if using AILZ integrated mode), @@ -246,6 +274,22 @@ module cosmos 'modules/cosmos.bicep' = { } } +// ========== COSMOS DB DNS ZONE GROUP (EXPLICIT CREATION) ========== +// Create DNS zone group explicitly to ensure A records are registered in private DNS zone +// Workaround: AVM cosmos module 0.13.1 does not properly handle privateDnsZoneGroups conditional +// This separate module creates the DNS zone group after the private endpoint exists +module cosmosDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'cosmos-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${cosmosAccountName}-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.cosmos + location: location + } + dependsOn: [ + cosmos + ] +} + // ========== APP CONFIGURATION ========== // Create new App Configuration store, with private endpoint support (if using AILZ integrated mode), // or use public endpoints (basic mode) @@ -264,6 +308,22 @@ module appConfigStore 'modules/app-config-store.bicep' = { } } +// ========== APP CONFIGURATION DNS ZONE GROUP (EXPLICIT CREATION) ========== +// Create DNS zone group explicitly to ensure A records are registered in private DNS zone +// Workaround: AVM app-configuration module 0.9.2 does not properly handle privateDnsZoneGroups conditional +// This separate module creates the DNS zone group after the private endpoint exists +module appConfigDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'appconfig-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${appConfigStoreName}-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.appConfig + location: location + } + dependsOn: [ + appConfigStore + ] +} + // ========== APP CONFIGURATION KEYS ========== // Basic mode: Create keys via Bicep (public access enabled, ARM can access) // AILZ mode: Skip Bicep creation, keys created via postprovision hook (jumpbox has VNet access) From 45ed687f8c371624731c9dd6ed8bf41b20cd36ff Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 20:03:09 -0400 Subject: [PATCH 14/29] Private Endpoint DNS Zone Groups Not Created --- changes.md | 62 +++++++++++++++++++++++++++++ infra/bicep/main.bicep | 4 ++ infra/scripts/get-ailz-resources.sh | 3 ++ 3 files changed, 69 insertions(+) diff --git a/changes.md b/changes.md index 7e1c09f..95a02ca 100644 --- a/changes.md +++ b/changes.md @@ -9,6 +9,7 @@ - [5. Fix: Container Apps Timeout - DNS Zone Group Creation Workaround](#5-fix-container-apps-timeout---dns-zone-group-creation-workaround) - [6. Fix: App Configuration Keys Failure - Private Network Access Limitation](#6-fix-app-configuration-keys-failure---private-network-access-limitation) - [7. Fix: Storage Account Name Output Returns Module Name Instead of Resource Name](#7-fix-storage-account-name-output-returns-module-name-instead-of-resource-name) +- [Solution Dependencies](#solution-dependencies) --- @@ -1064,3 +1065,64 @@ Audited all other modules that use the same pattern. All others correctly use `m ✅ **App Configuration keys** contain correct storage account name ✅ **Container Apps** can connect to storage successfully ✅ **No breaking changes** — existing code in `main.bicep` uses variable `storageAccountName` for App Config keys (basic mode), not affected by this output + +--- + +## Solution Dependencies + +Some fixes depend on others to function correctly. Understanding these dependencies is important for deployment sequencing and troubleshooting. + +### Dependency Graph + +``` +Items 1-4 (Independent) + ↓ +Item 5: DNS Zone Groups (Foundation) + ↓ + ├──→ Item 6: App Config Keys Postprovision Hook (Requires DNS resolution) + └──→ Item 7: Storage Account Name Output (Independent bug, but output consumed by Item 6) +``` + +### Dependency Details + +#### **Item 5 enables Item 6** (Critical Dependency) + +**Item 5** (DNS Zone Groups) is a **prerequisite** for **Item 6** (App Config Keys Postprovision Hook): + +- **Without Item 5:** Postprovision hook fails with DNS resolution errors + ``` + Failed to resolve 'sta5s63braj5xj2.queue.core.windows.net' + Failed to resolve 'appcs-a5s63braj5xj2.azconfig.io' + ``` + +- **With Item 5:** DNS zone groups register A records → jumpbox can resolve FQDNs → postprovision hook succeeds + +**Why:** The postprovision hook runs from the jumpbox VM (inside VNet) and must access Storage Account and App Configuration via private endpoints. Without DNS zone groups, there are no A records in Private DNS Zones, so DNS resolution fails. + +#### **Item 7 consumed by Item 6** (Data Dependency) + +**Item 7** (Storage Account Name Output) is **independent** but its output is **consumed by Item 6**: + +- **Without Item 7:** Wrong storage account name (`storage-xyz.storageAccount`) inserted into App Config +- **With Item 7:** Correct storage account name (`sta5s63braj5xj2`) inserted into App Config + +**Why:** The postprovision hook reads `STORAGE_ACCOUNT_NAME` from azd environment (populated from Bicep output) and writes it to App Configuration keys. Container Apps read this value to connect to blob storage and queues. + +#### **Items 1-4 are Independent** + +These fixes have no dependencies on other items: + +- **Item 1** (LogAnalytics CustomerId): Standalone Bicep reference() function fix +- **Item 2** (Property Name Typo): Simple typo fix in parameter name +- **Item 3** (UnmatchedPrincipalType): Standalone deployer() principalType detection +- **Item 4** (ACR Configuration Simplification): Architectural cleanup, no dependencies + +### Deployment Order (Recommended) + +All items are implemented in Bicep code and deployed together via `azd provision`. The dependency resolution happens automatically: + +1. **Bicep Provisioning** deploys Items 1-7 (in dependency order managed by Bicep's `dependsOn`) +2. **Item 5 modules execute** after resource modules (explicit `dependsOn: [storage]`, `dependsOn: [appConfigStore]`, etc.) +3. **Postprovision Hook** (Item 6) executes after Bicep completes, relies on Item 5 DNS + Item 7 outputs + +**No manual sequencing needed** — the Bicep module dependencies and azd hooks ensure correct execution order. diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index eabf3de..cef6710 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -42,6 +42,9 @@ param existingCognitiveServicesPrivateDnsZoneId string = '' @description('Resource ID of existing Blob Storage Private DNS Zone (required for ailz-integrated mode)') param existingBlobPrivateDnsZoneId string = '' +@description('Resource ID of existing Queue Storage Private DNS Zone (required for ailz-integrated mode)') +param existingQueuePrivateDnsZoneId string = '' + @description('Resource ID of existing Cosmos DB Private DNS Zone (required for ailz-integrated mode)') param existingCosmosPrivateDnsZoneId string = '' @@ -130,6 +133,7 @@ var networkConfig = isAILZIntegrated ? { privateDnsZoneIds: { cognitiveServices: existingCognitiveServicesPrivateDnsZoneId blob: existingBlobPrivateDnsZoneId + queue: existingQueuePrivateDnsZoneId cosmos: existingCosmosPrivateDnsZoneId appConfig: existingAppConfigPrivateDnsZoneId acr: existingAcrPrivateDnsZoneId diff --git a/infra/scripts/get-ailz-resources.sh b/infra/scripts/get-ailz-resources.sh index 2ce21eb..fc4ae56 100755 --- a/infra/scripts/get-ailz-resources.sh +++ b/infra/scripts/get-ailz-resources.sh @@ -177,6 +177,9 @@ get_resource_id "Cognitive Services Private DNS Zone" "EXISTING_COGNITIVE_SERVIC get_resource_id "Blob Storage Private DNS Zone" "EXISTING_BLOB_PRIVATE_DNS_ZONE_ID" \ "az network private-dns zone show --name privatelink.blob.core.windows.net --resource-group $AILZ_RG --query id -o tsv" +get_resource_id "Queue Storage Private DNS Zone" "EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID" \ + "az network private-dns zone show --name privatelink.queue.core.windows.net --resource-group $AILZ_RG --query id -o tsv" + get_resource_id "Cosmos DB Private DNS Zone" "EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID" \ "az network private-dns zone show --name privatelink.documents.azure.com --resource-group $AILZ_RG --query id -o tsv" From 6519540bbae5e714ebd1e89b575a460eebfa2b6a Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 20:13:07 -0400 Subject: [PATCH 15/29] feat(item-8): add conditional Queue Storage DNS Zone Group support --- changes.md | 301 ++++++++++++++++++++++++++++++++++++++++- infra/bicep/main.bicep | 2 +- 2 files changed, 299 insertions(+), 4 deletions(-) diff --git a/changes.md b/changes.md index 95a02ca..633a554 100644 --- a/changes.md +++ b/changes.md @@ -9,6 +9,7 @@ - [5. Fix: Container Apps Timeout - DNS Zone Group Creation Workaround](#5-fix-container-apps-timeout---dns-zone-group-creation-workaround) - [6. Fix: App Configuration Keys Failure - Private Network Access Limitation](#6-fix-app-configuration-keys-failure---private-network-access-limitation) - [7. Fix: Storage Account Name Output Returns Module Name Instead of Resource Name](#7-fix-storage-account-name-output-returns-module-name-instead-of-resource-name) +- [8. Fix: Queue Storage Private DNS Zone Missing in AILZ Resource Group](#8-fix-queue-storage-private-dns-zone-missing-in-ailz-resource-group) - [Solution Dependencies](#solution-dependencies) --- @@ -1068,6 +1069,285 @@ Audited all other modules that use the same pattern. All others correctly use `m --- +## 8. Fix: Queue Storage Private DNS Zone Missing in AILZ Resource Group + +**Error:** + +``` +LinkedInvalidPropertyId: Property id '' at path 'properties.privateDnsZoneConfigs[0].properties.privateDnsZoneId' +is invalid. Expect fully qualified resource Id that start with '/subscriptions/{subscriptionId}' +or '/providers/{resourceProviderNamespace}/'. +``` + +**Symptoms:** + +- Deployment fails after all resources provision successfully +- Error occurs during `storageQueueDnsZoneGroup` module creation +- `privateDnsZoneId` parameter is empty (`''`) +- Script `get-ailz-resources.sh` cannot find Queue Storage DNS Zone +- Azure Portal shows AILZ resource group (`rg-mini-ailz-jx05`) has only 6 Private DNS Zones: + - ✅ `privatelink.azconfig.io` + - ✅ `privatelink.azurecr.io` + - ✅ `privatelink.azurecontainerapps.io` + - ✅ `privatelink.blob.core.windows.net` + - ✅ `privatelink.cognitiveservices.azure.com` + - ✅ `privatelink.documents.azure.com` + - ❌ `privatelink.queue.core.windows.net` **(MISSING)** + +--- + +**Root Cause:** + +Azure Storage Account creates **two separate private endpoints** when network isolation is enabled: +1. **Blob endpoint** (`privatelink.blob.core.windows.net`) — ✅ Exists in AILZ RG +2. **Queue endpoint** (`privatelink.queue.core.windows.net`) — ❌ Does NOT exist in AILZ RG + +Both endpoints are created by ContentFlow deployment, but only the Blob DNS Zone was provisioned in the AILZ resource group. Without the Queue DNS Zone, the `storageQueueDnsZoneGroup` module cannot create the DNS zone group configuration, causing deployment failure. + +**Why This Wasn't Caught Earlier:** + +Items 1-7 focused on **existing** Private DNS Zones (ACR, Blob, Cosmos, App Config). The Queue DNS Zone requirement was assumed to exist alongside Blob, but AILZ infrastructure only provisioned Blob zone initially. + +--- + +**Solution: Conditional DNS Zone Group Creation** + +Since the Queue Storage Private DNS Zone is **infrastructure that must be created by the AILZ administrator** (not by this deployment template), we make the Queue DNS Zone Group creation **conditional** — it only executes if the DNS Zone exists. + +### Files Changed + +#### **Modified: `infra/bicep/main.bicep`** (line 247) + +**Before:** +```bicep +module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { + name: 'storage-queue-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${storageAccountName}-queue-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.queue + location: location + } + dependsOn: [storage] +} +``` + +**After:** +```bicep +module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated && !empty(networkConfig.privateDnsZoneIds.queue)) { + name: 'storage-queue-dns-zone-group-${resourceToken}' + params: { + privateEndpointName: '${storageAccountName}-queue-pe' + privateDnsZoneId: networkConfig.privateDnsZoneIds.queue + location: location + } + dependsOn: [storage] +} +``` + +**Explanation:** + +Added `&& !empty(networkConfig.privateDnsZoneIds.queue)` condition: +- **Before:** Module always executes in AILZ mode (even with empty DNS zone ID → deployment fails) +- **After:** Module only executes if DNS zone ID is not empty (deployment succeeds, DNS zone group skipped) + +This allows deployment to proceed without error while the Queue DNS Zone is missing, but maintains the correct configuration when the zone exists. + +#### **Modified: `infra/bicep/main.bicep`** (parameter added at line ~44) + +**Added:** +```bicep +@description('Resource ID of existing Queue Storage Private DNS Zone (required for ailz-integrated mode)') +param existingQueuePrivateDnsZoneId string = '' +``` + +#### **Modified: `infra/bicep/main.bicep`** (networkConfig object at line ~136) + +**Added:** +```bicep +privateDnsZoneIds: { + cognitiveServices: existingCognitiveServicesPrivateDnsZoneId + blob: existingBlobPrivateDnsZoneId + queue: existingQueuePrivateDnsZoneId // ← Added + cosmos: existingCosmosPrivateDnsZoneId + appConfig: existingAppConfigPrivateDnsZoneId + acr: existingAcrPrivateDnsZoneId + keyVault: existingKeyVaultPrivateDnsZoneId + containerAppsEnv: existingContainerAppsEnvPrivateDnsZoneId +} +``` + +#### **Modified: `infra/scripts/get-ailz-resources.sh`** (line ~180) + +**Added:** +```bash +get_resource_id "Queue Storage Private DNS Zone" "EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID" \ + "az network private-dns zone show --name privatelink.queue.core.windows.net --resource-group $AILZ_RG --query id -o tsv" +``` + +--- + +**Impact: Deployment Will Succeed BUT Queue Access Will Fail** + +With this fix: + +✅ **Deployment completes successfully** (no more `LinkedInvalidPropertyId` error) +✅ **All resources provision** (Storage Account, Container Apps, etc.) +✅ **Blob endpoint works** (has DNS zone + DNS zone group) +❌ **Queue endpoint DOES NOT WORK** (no DNS resolution) +❌ **Worker service cannot access queue** (cannot process content) + +**Why Queue Access Fails Without DNS Zone:** + +``` +Worker Container App (in VNet) + ↓ +Tries to connect: sta5s63braj5xj2.queue.core.windows.net + ↓ +DNS resolution fails (no A record in Private DNS Zone) + ↓ +Falls back to public DNS → gets public IP + ↓ +Storage Account has publicNetworkAccess: 'Disabled' + ↓ +Connection refused → HTTPSConnectionError +``` + +Without the DNS zone group, there's no A record registered in Private DNS Zone. DNS resolution fails, and the Worker cannot access the queue endpoint even though the private endpoint exists. + +--- + +**Required Action: Request DNS Zone Creation from AILZ Administrator** + +To fully resolve this issue, the AILZ administrator must create the missing Private DNS Zone. + +### Request Context for Administrator + +**What's Needed:** +- Create Private DNS Zone: `privatelink.queue.core.windows.net` +- Location: AILZ Resource Group `rg-mini-ailz-jx05` +- Link to VNet: Same VNet used for other Private DNS Zones +- Reason: Storage Account creates 2 private endpoints (blob + queue), each requires its own DNS zone + +**Technical Context:** + +Azure Storage Account with private endpoints enabled creates **two separate service endpoints**: +1. **Blob Service** (`*.blob.core.windows.net`) — DNS zone exists ✅ +2. **Queue Service** (`*.queue.core.windows.net`) — DNS zone missing ❌ + +Without both DNS zones, ContentFlow Worker service cannot access Azure Storage Queue, which is required for asynchronous content processing tasks. + +**Impact:** +- **Current:** Deployment succeeds but Worker service non-functional (cannot process content) +- **After DNS Zone Created:** Worker can access queue → full functionality restored + +### Azure CLI Commands for Administrator + +```bash +# 1. Create Queue Storage Private DNS Zone +az network private-dns zone create \ + --resource-group rg-mini-ailz-jx05 \ + --name privatelink.queue.core.windows.net + +# 2. Link DNS Zone to VNet (replace with actual VNet name) +az network private-dns link vnet create \ + --resource-group rg-mini-ailz-jx05 \ + --zone-name privatelink.queue.core.windows.net \ + --name ailz-vnet-link \ + --virtual-network \ + --registration-enabled false +``` + +**After DNS Zone Created:** + +1. Development team re-runs discovery script on jumpbox: + ```bash + cd ~/contentflow + ./infra/scripts/get-ailz-resources.sh + ``` + +2. Verify Queue DNS Zone ID is set: + ```bash + azd env get-value EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID + # Should return: /subscriptions/.../privateDnsZones/privatelink.queue.core.windows.net + ``` + +3. Re-deploy to create DNS zone group: + ```bash + azd provision --no-prompt + ``` + +4. Verify DNS zone group created: + ```bash + az network private-endpoint dns-zone-group list \ + --resource-group rg-contentflow-test004-jx05 \ + --endpoint-name sta5s63braj5xj2-queue-pe + ``` + +5. Verify A record registered: + ```bash + az network private-dns record-set a list \ + --resource-group rg-mini-ailz-jx05 \ + --zone-name privatelink.queue.core.windows.net + # Should show A record for sta5s63braj5xj2 → 10.0.0.x + ``` + +6. Test DNS resolution from jumpbox: + ```bash + nslookup sta5s63braj5xj2.queue.core.windows.net + # Should resolve to private IP (10.0.0.x) + ``` + +--- + +**Why Not Make Queue Storage Optional?** + +The Worker service requires Azure Storage Queue for task distribution and content processing orchestration. Without queue access, the core functionality of ContentFlow does not work. + +**Alternatives Considered:** +- ❌ **Enable public network access** — Violates Zero Trust / network isolation policies +- ❌ **Use only blob storage** — Requires major application architecture changes +- ❌ **Remove worker service** — ContentFlow becomes non-functional (cannot process content) +- ✅ **Request DNS Zone creation** — Minimal infrastructure change, preserves security, enables full functionality + +--- + +**Expected Results After DNS Zone Created and Redeployed:** + +✅ All 5 DNS zone groups exist (ACR, Blob, **Queue**, Cosmos, App Config) +✅ All 5 Private DNS Zones have A records registered +✅ Worker service can access Storage Queue via private endpoint +✅ Content processing workflows function correctly +✅ Complete end-to-end deployment successful + +--- + +**Verification After Full Fix:** + +```bash +# 1. Verify all 5 DNS zone groups exist +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -blob-pe +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -queue-pe # ← This one +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe +az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe + +# 2. Verify A records in all 5 Private DNS Zones +az network private-dns record-set a list --resource-group --zone-name privatelink.azurecr.io +az network private-dns record-set a list --resource-group --zone-name privatelink.blob.core.windows.net +az network private-dns record-set a list --resource-group --zone-name privatelink.queue.core.windows.net # ← This one +az network private-dns record-set a list --resource-group --zone-name privatelink.documents.azure.com +az network private-dns record-set a list --resource-group --zone-name privatelink.azconfig.io + +# 3. Test worker queue access (check Container App logs) +az containerapp logs show \ + --name worker- \ + --resource-group \ + --follow \ + --query "[?contains(Log, 'Queue')]" +``` + +--- + ## Solution Dependencies Some fixes depend on others to function correctly. Understanding these dependencies is important for deployment sequencing and troubleshooting. @@ -1080,7 +1360,8 @@ Items 1-4 (Independent) Item 5: DNS Zone Groups (Foundation) ↓ ├──→ Item 6: App Config Keys Postprovision Hook (Requires DNS resolution) - └──→ Item 7: Storage Account Name Output (Independent bug, but output consumed by Item 6) + ├──→ Item 7: Storage Account Name Output (Independent bug, but output consumed by Item 6) + └──→ Item 8: Queue Storage DNS Zone (Infrastructure prerequisite - BLOCKED) ``` ### Dependency Details @@ -1108,6 +1389,17 @@ Item 5: DNS Zone Groups (Foundation) **Why:** The postprovision hook reads `STORAGE_ACCOUNT_NAME` from azd environment (populated from Bicep output) and writes it to App Configuration keys. Container Apps read this value to connect to blob storage and queues. +#### **Item 8 blocks Worker functionality** (Infrastructure Dependency) + +**Item 8** (Queue Storage DNS Zone) is **BLOCKED** by missing infrastructure in AILZ resource group: + +- **Without Item 8:** Deployment succeeds but Worker service cannot access Storage Queue (no DNS resolution) +- **With Item 8:** Worker can access queue → full ContentFlow functionality + +**Why:** Storage Account creates two private endpoints (blob + queue). Item 5 implemented support for both DNS zone groups, but the Queue DNS Zone itself does not exist in AILZ RG. This is an **infrastructure prerequisite** that must be created by AILZ administrator before Worker service can function. + +**Current Status:** Code is ready (Items 5 + 8 completed), awaiting DNS zone creation by admin. + #### **Items 1-4 are Independent** These fixes have no dependencies on other items: @@ -1121,8 +1413,11 @@ These fixes have no dependencies on other items: All items are implemented in Bicep code and deployed together via `azd provision`. The dependency resolution happens automatically: -1. **Bicep Provisioning** deploys Items 1-7 (in dependency order managed by Bicep's `dependsOn`) +1. **Bicep Provisioning** deploys Items 1-8 (in dependency order managed by Bicep's `dependsOn`) 2. **Item 5 modules execute** after resource modules (explicit `dependsOn: [storage]`, `dependsOn: [appConfigStore]`, etc.) -3. **Postprovision Hook** (Item 6) executes after Bicep completes, relies on Item 5 DNS + Item 7 outputs +3. **Item 8 Queue DNS Zone Group** conditionally skipped (DNS zone doesn't exist yet) +4. **Postprovision Hook** (Item 6) executes after Bicep completes, relies on Item 5 DNS + Item 7 outputs **No manual sequencing needed** — the Bicep module dependencies and azd hooks ensure correct execution order. + +**Note:** Item 8 (Queue Storage DNS Zone) requires AILZ administrator action before full deployment can succeed. Current deployment completes but Worker service remains non-functional until Queue DNS Zone is created. diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index cef6710..b085650 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -244,7 +244,7 @@ module storageBlobDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = ] } -module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { +module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated && !empty(networkConfig.privateDnsZoneIds.queue)) { name: 'storage-queue-dns-zone-group-${resourceToken}' params: { privateEndpointName: '${storageAccountName}-queue-pe' From 0eb752521f14b08411f672cfd56e7f5a8ad7d032 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 20 Mar 2026 20:16:50 -0400 Subject: [PATCH 16/29] fix: use parameter instead of networkConfig for queue DNS zone check Prevents compilation error in basic mode where networkConfig.privateDnsZoneIds does not exist --- infra/bicep/main.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index b085650..a524235 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -244,7 +244,7 @@ module storageBlobDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = ] } -module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated && !empty(networkConfig.privateDnsZoneIds.queue)) { +module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated && !empty(existingQueuePrivateDnsZoneId)) { name: 'storage-queue-dns-zone-group-${resourceToken}' params: { privateEndpointName: '${storageAccountName}-queue-pe' From 5ce109a903606eac54aebe90d6e957af0cb735f4 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Sat, 21 Mar 2026 07:52:20 -0400 Subject: [PATCH 17/29] docs: update README to reflect current implementation - Remove ACR reuse references (always creates new ACR per workload) - Add Queue Storage DNS Zone to all relevant sections - Add troubleshooting section for missing Queue Storage DNS Zone - Add important note about Queue DNS Zone requirement in AILZ mode - Update manual configuration steps with Queue DNS Zone ID - Fix inconsistent ACR descriptions in deployment sections - Ensure all documentation aligns with changes.md Items 4 and 8 --- infra/README.md | 74 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/infra/README.md b/infra/README.md index 436e91e..83ecfaa 100644 --- a/infra/README.md +++ b/infra/README.md @@ -20,7 +20,7 @@ ContentFlow supports **two deployment configurations** based on your infrastruct | **Compliance Ready** | N/A | ✅ Yes | | **Enterprise Use** | Development | Production | | **Cost** | Lower | Slightly higher (Network infra costs added) | -| **Container Registry** | Creates new ACR | Can reuse existing AI LZ ACR | +| **Container Registry** | Creates new ACR | Creates new ACR | | **Prerequisites** | Azure subscription | Azure subscription + AI LZ | --- @@ -158,7 +158,7 @@ pwsh .\get-ailz-resources.ps 2. ✅ Asks for your AI Landing Zone resource group name 3. ✅ Discovers all required resources automatically: - Virtual Network ID and Subnet Names - - Private DNS Zone IDs (blob, cosmos, acr, app config, cognitive services, key vault, container apps environment) + - Private DNS Zone IDs (blob, queue, cosmos, acr, app config, cognitive services, key vault, container apps environment) - JumpBox VM information (if available) - Log Analytics Workspace ID (optional) - Application Insights ID (optional) @@ -175,6 +175,7 @@ EXISTING_VNET_RESOURCE_ID="/subscriptions/.../virtualNetworks/my-vnet" PRIVATE_ENDPOINT_SUBNET_NAME="pe-subnet" CONTAINER_APPS_SUBNET_NAME="aca-env-subnet" EXISTING_BLOB_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.blob.core.windows.net" +EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.queue.core.windows.net" EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.documents.azure.com" JUMPBOX_VM_RESOURCE_ID="/subscriptions/.../resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/jumpbox" JUMPBOX_VM_NAME="jumpbox" @@ -260,7 +261,7 @@ This will: **What Gets Deployed:** - Container Apps with private endpoints -- Container Registry (new, or private endpoint to existing ACR) +- Container Registry with private endpoints - Storage Account with private endpoints - Cosmos DB with private endpoints - App Configuration with private endpoints @@ -274,6 +275,7 @@ This will: - Existing Private DNS Zones: - Cognitive Services - Blob Storage + - Queue Storage - Cosmos DB - App Configuration - Container Registry @@ -282,6 +284,8 @@ This will: - Existing Log Analytics Workspace (optional) - Existing Application Insights (optional) +> **⚠️ Important Note on Queue Storage DNS Zone**: Some AI Landing Zone deployments may not include the `privatelink.queue.core.windows.net` DNS Zone. The ContentFlow deployment will succeed without it, but the Worker service will not function until this zone is created. If the helper script reports that the Queue Storage DNS Zone is missing, coordinate with your AILZ administrator to create it. See the [troubleshooting section](#troubleshooting) for details. + **Duration:** 10-15 minutes (depends on existing AI LZ setup) --- @@ -400,6 +404,8 @@ azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID /subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net +azd env set EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID /subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.queue.core.windows.net + azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID /subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.documents.azure.com azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID /subscriptions//resourceGroups//providers/Microsoft.Network/privateDnsZones/privatelink.azconfig.io @@ -521,7 +527,7 @@ infra/ Main infrastructure template that provisions: - Resource Group (if not already existing) - Managed Identity (for Container Apps) -- Container Registry (or references existing ACR with private endpoint and RBAC) +- Container Registry - Container Apps Environment - Storage Account (blob + queue) - Cosmos DB with all required containers @@ -541,7 +547,7 @@ Each module is a reusable component: - **app-insights.bicep**: Application Insights for monitoring - **container-app.bicep**: Creates a Container App with configurable settings - **container-apps-environment.bicep**: Container Apps Environment -- **container-registry.bicep**: Azure Container Registry (skipped when using existing ACR) +- **container-registry.bicep**: Azure Container Registry - **cosmos-db.bicep**: Cosmos DB account with containers - **log-analytics-ws.bicep**: Log Analytics Workspace - **storage.bicep**: Storage Account with blob container and queue @@ -791,6 +797,64 @@ azd env new --- +#### 6. **Missing Queue Storage Private DNS Zone (AILZ Mode)** +``` +Error: LinkedInvalidPropertyId: Property id '' at path 'properties.privateDnsZoneConfigs[0].properties.privateDnsZoneId' is invalid +``` +**Or:** +``` +Worker logs show: Failed to resolve '.queue.core.windows.net' +``` + +**Symptoms:** +- Deployment completes successfully but Worker service cannot access Storage Queue +- DNS resolution fails for queue endpoint +- `get-ailz-resources.sh` script reports Queue Storage DNS Zone not found + +**Root Cause:** +Azure Storage Account with private endpoints requires **two DNS Zones**: +- ✅ Blob Storage (`privatelink.blob.core.windows.net`) - Usually exists in AILZ +- ❌ Queue Storage (`privatelink.queue.core.windows.net`) - May be missing in some AILZ deployments + +**Solution - Request AILZ Administrator to Create DNS Zone:** + +1. **Coordinate with AILZ Admin** to create the missing DNS Zone: + ```bash + # Create Queue Storage Private DNS Zone + az network private-dns zone create \ + --resource-group \ + --name privatelink.queue.core.windows.net + + # Link to AILZ VNet + az network private-dns link vnet create \ + --resource-group \ + --zone-name privatelink.queue.core.windows.net \ + --name ailz-vnet-link \ + --virtual-network \ + --registration-enabled false + ``` + +2. **After DNS Zone is created**, re-run configuration and deployment: + ```bash + # On JumpBox or local machine + cd contentflow/infra/scripts + ./get-ailz-resources.sh --auto-set + + # Re-deploy (will create DNS zone group and register A record) + azd provision --no-prompt + ``` + +3. **Verify DNS resolution** from JumpBox: + ```bash + nslookup .queue.core.windows.net + # Should resolve to private IP (10.0.0.x) + ``` + +**Workaround (Temporary - Reduced Functionality):** +If Queue DNS Zone creation is pending, ContentFlow API and Web will work, but Worker service will not be able to process content until DNS zone is created. + +--- + ### Checking Deployment Status ```bash From 548158530e0e29acdf14bc1b64f8658f6bc62738 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Sat, 21 Mar 2026 07:57:04 -0400 Subject: [PATCH 18/29] Docs update --- infra/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/infra/README.md b/infra/README.md index 83ecfaa..10ca3bc 100644 --- a/infra/README.md +++ b/infra/README.md @@ -20,7 +20,6 @@ ContentFlow supports **two deployment configurations** based on your infrastruct | **Compliance Ready** | N/A | ✅ Yes | | **Enterprise Use** | Development | Production | | **Cost** | Lower | Slightly higher (Network infra costs added) | -| **Container Registry** | Creates new ACR | Creates new ACR | | **Prerequisites** | Azure subscription | Azure subscription + AI LZ | --- From d099d7cedba1065397bc148e001c73beabf6705f Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Mon, 23 Mar 2026 14:38:12 -0400 Subject: [PATCH 19/29] Removing changes.md --- .DS_Store | Bin 6148 -> 10244 bytes changes.md | 1423 ---------------------------------------------------- 2 files changed, 1423 deletions(-) delete mode 100644 changes.md diff --git a/.DS_Store b/.DS_Store index 11765c4c72c09c28e71ba2b46a950db03450f6cf..5df274e9ed01e2d44968bd4dd626e2db05c65838 100644 GIT binary patch literal 10244 zcmeHNO>7%Q6n^8RiA`Fv`2`{nD=Z{L)7EJc)CyH`-5{DEkthx&AuaB%y>XVTcdXq_ zoF7G62{<4DQY9{M0WQF);y|myi34p_KahOsz>04HP_|+- zFR0@_fcvPS%!bkrBzMIzgWUrOT}imbAb~sftIQl_Hk5uK19u>SJCHCk33n)jM@K)E zn*+%PQr40I$v`~=8Ek5UlW3qJ$Y(vapu))x6jtwl$zR9 zm&9{|_izYY364V5HY;`;l}9YPdJ8ME^<-AO9MNGiJZK$x0Q(N z(VuQzyM3MG+G9Ks+y0g@e1zj;d9EV5BBCtSo%l?Id|S1g}1AMc+}4WF2<*W7Bj(OTZqycIX6IScG=7C(Y|q-**G)2J1c-gDIUG}AU5?qHS-G`aY^X_sg% zM_25U%YE}fM>3U66_kO^&0|BEQ=^0GaOU*Zpt^bbWM)tu8Gd4GE0yd^pLlYnuxwRq z`Z`MlzxRaSkKjmfzg@q9T4O$Wo@Q}1%#(Z><@vp$M46`U={S7k-cF^fyQizStFN!G z|GxVl7#Mi4p!Dckak=cU49-!DIyq~_m@|vi&YK&?oM)_gOerYcyXD_7IV?FH{hP_aB%+l22qHB0_f<9kRI^Ci}Eo+*(re{)n{-TTIPk2UU!d|2paTST5 zswMd9=bT*=U%^v}3T5cJ>-M;}{)x<2h` z?jcUuky~Tl&EOr_B#t;`5AGG4vi&~Gt4W;IyklF`E~6!5rMuw*T*HOffn}WLPK*Qc zi+xZ(k%=F_{U}bEeF-is`1OJc#(mI|W&!Ka) zf!I9`J8M_*x{Mbq%+3!acfpDu$D`!O@p!|I2 Rl;r+DIERw^|Nq;roApH>Gx3xK7v<&T=cR+xPb|E+n4N<|kQvAU0s(Fy;R-TzW8rt^ X$^0swAS)P{Al89gz_2- --endpoint-name -pe -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -blob-pe -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -queue-pe -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe - -# 2. Verify A records registered in Private DNS Zones -az network private-dns record-set a list --resource-group --zone-name privatelink.azurecr.io -az network private-dns record-set a list --resource-group --zone-name privatelink.blob.core.windows.net -az network private-dns record-set a list --resource-group --zone-name privatelink.queue.core.windows.net -az network private-dns record-set a list --resource-group --zone-name privatelink.documents.azure.com -az network private-dns record-set a list --resource-group --zone-name privatelink.azconfig.io - -# 3. Test DNS resolution from within VNet (e.g., from jumpbox) -nslookup .azurecr.io -nslookup .blob.core.windows.net -nslookup .queue.core.windows.net -nslookup .documents.azure.com -nslookup .azconfig.io -# All should resolve to 10.0.0.x private IPs -``` - ---- - -**Verification Commands:** - -```bash -# Verify DNS zone group exists -az network private-endpoint dns-zone-group list \ - --resource-group \ - --endpoint-name -pe - -# Verify A records registered -az network private-dns record-set a list \ - --resource-group \ - --zone-name privatelink.azurecr.io \ - --query "[].{Name:name, IP:aRecords[0].ipv4Address}" -``` - ---- - -**Fix Verification Results (Deployment: content_flow_Friday20-02)** - -**Tested**: March 20, 2026 -**Environment**: AILZ-integrated mode with jumpbox deployment - -✅ **DNS Zone Group Created Successfully** -```json -{ - "name": "default", - "privateDnsZoneConfigs": [ - { - "name": "privatelink-azurecr-io", - "privateDnsZoneId": "/subscriptions/.../privateDnsZones/privatelink.azurecr.io", - "recordSets": [ - { - "fqdn": "cra5s63braj5xj2.eastus2.data.privatelink.azurecr.io", - "ipAddresses": ["10.0.0.15"], - "provisioningState": "Succeeded", - "recordType": "A" - }, - { - "fqdn": "cra5s63braj5xj2.privatelink.azurecr.io", - "ipAddresses": ["10.0.0.16"], - "provisioningState": "Succeeded", - "recordType": "A" - } - ] - } - ], - "provisioningState": "Succeeded" -} -``` - -✅ **A Records Registered in Private DNS Zone** -``` -Name IP ---------------- --------- -cra5s63braj5xj2 10.0.0.16 -``` - -✅ **Container Apps Deployed Successfully** -- Worker: 16.4 seconds (previously: 20-minute timeout) -- API: 49.4 seconds (previously: 20-minute timeout) -- Web: 18.5 seconds (previously: 20-minute timeout) - -**Result**: The workaround with explicit DNS zone group module completely resolved the Container Apps timeout issue. DNS records are now automatically registered, and Container Apps can successfully pull images from the private ACR. - ---- - -## 6. Fix: App Configuration Keys Failure - Private Network Access Limitation - -**Error:** - -``` -appConfigKeys-a5s63braj5xj2 → Failed -Error: Forbidden - Access to the requested resource is forbidden -``` - -**Symptoms:** - -- App Configuration Store provisions successfully in AILZ mode -- Private endpoint created and functional -- Container Apps can access App Config via private endpoint -- **BUT**: Bicep module `appConfigStoreKeys` fails with 403 Forbidden when attempting to create key-value pairs -- Error persists even after RBAC role assignments have propagated (2+ hours) -- Error appears for **every key** being created (30+ Forbidden errors) - ---- - -**Root Cause:** - -This is **NOT an RBAC issue** — it's a **network connectivity limitation** with Azure Resource Manager (ARM) deployment service accessing App Configuration through private endpoints. - -In AILZ-integrated deployments, the App Configuration Store is configured with: -- `publicNetworkAccess: 'Disabled'` — no public internet access ✅ (correct for AILZ) -- Private endpoint in the VNet — accessible only through private network ✅ -- `dataPlaneProxy.authenticationMode: 'Pass-through'` — uses deployer MI for auth ✅ -- `dataPlaneProxy.privateLinkDelegation: 'Enabled'` — enables ARM private network access ✅ - -**The problem:** When Bicep creates `Microsoft.AppConfiguration/configurationStores/keyValues` resources, the **ARM deployment service** (which runs in Microsoft's network, **outside the customer's VNet**) attempts to write the key-values to the App Configuration data plane. However: - -1. ARM deployment service has no direct access to the customer's private VNet -2. App Configuration has `publicNetworkAccess: 'Disabled'` (no internet route) -3. ARM cannot connect → 403 Forbidden - ---- - -**Microsoft Official Documentation:** - -From [Azure App Configuration - Deployment Overview](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-deployment-overview?tabs=portal#private-network-access): - -> **"Private network access"** -> -> When you restrict an App Configuration resource to private network access, deployments that access App Configuration data through public networks are blocked. For deployments to succeed when access to an App Configuration resource is restricted to private networks, you must take the following actions: -> -> 1. **Set up an Azure Resource Management private link** -> 2. In your App Configuration resource, set the Azure Resource Manager authentication mode to **Pass-through** -> 3. In your App Configuration resource, **enable Azure Resource Manager private network access** (this is `privateLinkDelegation: 'Enabled'`) -> 4. **Run deployments accessing App Configuration data through the configured Azure Resource Manager private link** - ---- - -**Why `privateLinkDelegation: 'Enabled'` Alone Is Not Sufficient:** - -The `dataPlaneProxy.privateLinkDelegation` property (API version `2025-02-01-preview` or later) **enables** App Configuration to accept ARM connections via private link, but **does not establish the private link**. It's a prerequisite, not a complete solution. - -To **actually use** this feature, you need an **Azure Resource Manager Private Link** (requirement #1 and #4 above), which is: - -- **An enterprise infrastructure** configured at the **root management group** level (applies to the entire tenant) -- **Out of scope** for application-level templates like ContentFlow -- Requires **Global Administrator** or **Owner permissions at the root management group** -- See: [Create Azure Resource Manager Private Link](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/create-private-link-access-portal) - -**Without ARM Private Link configured at the tenant level**, even with `privateLinkDelegation: 'Enabled'`, ARM deployment service still attempts to access App Configuration over the public internet, which fails when `publicNetworkAccess: 'Disabled'`. - ---- - -**Solution Architecture: Two-Phase Approach** - -Since ARM Private Link is an enterprise-level prerequisite beyond the scope of this template, we implement a **two-phase solution** based on deployment mode: - -### **Basic Mode** (Public Deployment) -- App Configuration: `publicNetworkAccess: 'Enabled'` (no private endpoints) -- **Keys created via Bicep**: ARM can access over the internet → Success ✅ -- No postprovision hook needed - -### **AILZ Mode** (Private Deployment) -- App Configuration: `publicNetworkAccess: 'Disabled'` + Private Endpoint -- **Keys created via postprovision hook**: Script runs on jumpbox VM (inside VNet) → Has private endpoint access → Success ✅ -- Bicep module skipped (conditional: `if (!isAILZIntegrated)`) - -**Why the Hook Works:** - -``` -┌─────────────────────────────────────────────────────────┐ -│ Azure Resource Manager (ARM) Deployment Service │ -│ (Runs in Microsoft's network, NOT in customer VNet) │ -└──────────────────┬──────────────────────────────────────┘ - │ - │ Bicep tries: keyValues resource - │ Result: 403 Forbidden ❌ - │ (no ARM Private Link configured) - ▼ - ┌───────────────────────────┐ - │ App Configuration Store │ - │ • publicNetworkAccess: │ - │ 'Disabled' │ - │ • privateLinkDelegation: │ - │ 'Enabled' │ - └───────────────────────────┘ - ▲ - │ - │ postprovision hook: az appconfig kv set - │ Result: Success ✅ - │ (jumpbox has VNet access) - │ - ┌───────────────────────────┐ - │ Jumpbox VM (in VNet) │ - │ • Private endpoint │ - │ • Runs post-provision.sh │ - └───────────────────────────┘ -``` - -The postprovision hook executes **after Bicep provisioning completes**, from the jumpbox VM which: -- Is located inside the customer's VNet -- Has network access via the App Configuration private endpoint -- Uses Azure CLI with `--auth-mode login` (managed identity authentication) -- Creates all 30 key-value pairs successfully - ---- - -**Implementation Details:** - -### File Changes - -#### **1. Modified: `infra/bicep/main.bicep`** (line 267) - -**Before:** -```bicep -module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = { - name: 'appConfigKeys-${resourceToken}' - params: { - appConfigStoreName: appConfigStoreName - configurationKeyValues: [...] - } -} -``` - -**After:** -```bicep -// Basic mode: Create keys via Bicep (public access enabled, ARM can access) -// AILZ mode: Skip Bicep creation, keys created via postprovision hook -module appConfigStoreKeys 'modules/app-config-store-keys.bicep' = if (!isAILZIntegrated) { - name: 'appConfigKeys-${resourceToken}' - params: { - appConfigStoreName: appConfigStoreName - configurationKeyValues: [...] - } -} -``` - -**Result:** Module only runs in basic mode. In AILZ mode, Bicep skips this module entirely → no 403 Forbidden errors. - -#### **2. Modified: `infra/bicep/modules/app-config-store.bicep`** - -**Kept conditional `privateLinkDelegation`** (implemented in previous fix): -```bicep -dataPlaneProxy: { - authenticationMode: 'Pass-through' - privateLinkDelegation: enablePrivateEndpoint ? 'Enabled' : 'Disabled' // Conditional -} -``` - -**Why keep it?** Even though it doesn't solve the **key creation** problem (requires ARM Private Link at tenant level), it: -- Follows Microsoft's documented best practice for AILZ deployments -- Prepares the App Configuration Store for enterprise environments that **do** have ARM Private Link configured -- Is a prerequisite (#3 in Microsoft's documentation) -- Does not cause harm when ARM Private Link is absent - -#### **3. Extended: `infra/scripts/post-provision.sh`** - -**Added new section** (after ACR import, lines ~55+): - -```bash -# Create App Configuration keys in AILZ mode -if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then - echo "✓ Creating App Configuration keys (AILZ mode - via jumpbox with VNet access)..." - - APP_CONFIG_NAME=$(azd env get-value APP_CONFIG_NAME) - COSMOS_DB_NAME=$(azd env get-value COSMOS_DB_NAME) - # ... get other values from azd env - - # Create all 30 keys using az appconfig kv set - az appconfig kv set --name "$APP_CONFIG_NAME" \ - --key "contentflow.common.COSMOS_DB_ENDPOINT" \ - --value "$COSMOS_ENDPOINT" \ - --auth-mode login --yes --only-show-errors - - # ... (30 total keys) - - # Verify creation - KEY_COUNT=$(az appconfig kv list --name "$APP_CONFIG_NAME" \ - --auth-mode login --query "length([])" -o tsv) - echo " ✓ Verification: $KEY_COUNT keys found" -fi -``` - -**Key Features:** -- Uses `--auth-mode login`: Authenticates with deployer managed identity -- Uses `--only-show-errors`: Suppresses verbose output -- Validates key count after creation -- Provides clear error messages if creation fails - -#### **4. Modified: `infra/bicep/main.bicep`** (outputs section) - -**Added output:** -```bicep -output APP_CONFIG_NAME string = appConfigStoreName -``` - -Required for the postprovision script to identify the correct App Configuration Store. - ---- - -**How It Works (AILZ Mode):** - -1. **Bicep Provisioning (`azd provision`)**: - - Creates App Configuration Store with private endpoint and `privateLinkDelegation: 'Enabled'` - - **Skips** `appConfigStoreKeys` module (conditional: `if (!isAILZIntegrated)`) - - No 403 Forbidden errors during Bicep deployment - - Completes successfully, outputs App Config name to azd environment - -2. **Postprovision Hook (`post-provision.sh`)**: - - Detects `DEPLOYMENT_MODE=ailz-integrated` - - Retrieves App Config name and all required values from azd environment - - Executes `az appconfig kv set` 30 times (one per key) - - Azure CLI accesses App Config **via private endpoint** (jumpbox is in VNet) - - All keys created successfully - - Verifies key count matches expected (30) - -3. **Container Apps Startup**: - - Apps read `AZURE_APP_CONFIG_ENDPOINT` from environment variables - - Connect to App Config via private endpoint - - Load all 30 keys successfully - - Applications start without configuration errors - ---- - -**Expected Results After Fix:** - -✅ **Basic mode** (public deployment): -- Bicep creates all 30 keys during `azd provision` -- No postprovision hook execution needed -- Single `azd up` command completes end-to-end - -✅ **AILZ mode** (private deployment): -- Bicep **skips** key creation (module not executed) -- No 403 Forbidden errors during Bicep phase -- Postprovision hook creates all 30 keys from jumpbox -- Container Apps read keys successfully via private endpoint -- Single `azd up` command completes end-to-end (hook is automatic) - -✅ **Security maintained**: -- App Configuration remains fully private (`publicNetworkAccess: 'Disabled'`) -- No temporary public access required -- Zero Trust principles preserved - ---- - -**Verification After Deployment (AILZ Mode):** - -```bash -# 1. Verify privateLinkDelegation is enabled (best practice) -az rest --method GET \ - --url "https://management.azure.com/subscriptions//resourceGroups//providers/Microsoft.AppConfiguration/configurationStores/?api-version=2025-02-01-preview" \ - --query "properties.dataPlaneProxy.privateLinkDelegation" -# Expected: "Enabled" - -# 2. Verify all 30 keys were created -az appconfig kv list \ - --name \ - --auth-mode login \ - --query "length([])" -# Expected: 30 - -# 3. Check specific keys -az appconfig kv list \ - --name \ - --auth-mode login \ - --query "[].{Key:key, Value:value}" \ - --output table - -# 4. Verify Container Apps can access config -az containerapp logs show \ - --name api- \ - --resource-group \ - --query "[?contains(Log, 'Configuration loaded')]" -``` - ---- - -**Why Not Enable ARM Private Link?** - -Azure Resource Manager Private Link is an **enterprise infrastructure prerequisite** that: - -- **Scope:** Configured at the **root management group** level (entire tenant) -- **Impact:** Applies to **all** Azure Resource Manager deployments across the tenant -- **Permissions Required:** - - Global Administrator for Microsoft Entra ID - - Owner or Contributor at the root management group - - Ability to elevate access and grant permissions across all subscriptions -- **Complexity:** Multi-step setup with private endpoints, private DNS zones, and link associations -- **Purpose:** Designed for enterprises with strict network isolation requirements across all Azure operations -- **Documentation:** [Use portal to create private link for managing Azure resources](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/create-private-link-access-portal) - -**For ContentFlow deployments:** -- This template targets **application-level** deployments, not tenant-wide infrastructure -- Most customers do **not** have ARM Private Link configured (it's optional and complex) -- The postprovision hook provides a **portable solution** that works regardless of ARM Private Link presence -- If customers **do** have ARM Private Link configured, `privateLinkDelegation: 'Enabled'` ensures compatibility - ---- - -### ✅ Chosen Solution: Postprovision Hook -**Why it's optimal:** -- ✅ Runs on existing jumpbox (no additional resources/cost) -- ✅ Has VNet access via private endpoint (already configured) -- ✅ Uses managed identity authentication (deployer MI, already assigned) -- ✅ Simple bash script with Azure CLI (standard tooling) -- ✅ Executes automatically as part of `azd up` (no manual steps) -- ✅ Maintains Zero Trust security (no public access) -- ✅ Portable across environments (doesn't require ARM Private Link) - ---- - -**Impact on Deployment Modes:** - -| Deployment Mode | Public Network Access | Keys Creation Method | ARM Private Link Required? | -|-----------------|----------------------|---------------------|---------------------------| -| **Basic** | `'Enabled'` | Bicep module | No | -| **AILZ** | `'Disabled'` | Postprovision hook | No (would enable it if present) | - ---- - -**References:** - -- [Azure App Configuration - Deployment Overview](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-deployment-overview) -- [Private network access for App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/quickstart-deployment-overview?tabs=portal#private-network-access) -- [Create Azure Resource Manager Private Link](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/create-private-link-access-portal) -- [Use private endpoints for Azure App Configuration](https://learn.microsoft.com/en-us/azure/azure-app-configuration/concept-private-endpoint) - ---- - -**Troubleshooting:** - -If App Configuration keys are not created in AILZ mode: - -```bash -# 1. Check postprovision hook execution -# Look for: "Creating App Configuration keys (AILZ mode...)" -azd provision --no-prompt 2>&1 | grep -A 10 "Creating App Configuration" - -# 2. Manually run key creation from jumpbox -APP_CONFIG_NAME="appcs-" -az appconfig kv set --name "$APP_CONFIG_NAME" \ - --key "contentflow.common.COSMOS_DB_ENDPOINT" \ - --value "" \ - --auth-mode login --yes - -# 3. Verify managed identity has correct role -az role assignment list \ - --scope "/subscriptions//resourceGroups//providers/Microsoft.AppConfiguration/configurationStores/$APP_CONFIG_NAME" \ - --query "[?roleDefinitionName=='App Configuration Data Owner'].principalId" - -# 4. Check private endpoint connectivity from jumpbox -nslookup .azconfig.io -# Should resolve to 10.0.0.x (private IP) -``` - ---- - -This solution provides a production-ready, secure, and automated approach for managing App Configuration keys in both basic and AILZ deployment modes without requiring enterprise-level ARM Private Link infrastructure. - ---- - -## 7. Fix: Storage Account Name Output Returns Module Name Instead of Resource Name - -**Error:** - -``` -ERROR: HTTPSConnection(host='storage-a5s63braj5xj2.storageaccount.queue.core.windows.net', port=443): -Failed to resolve 'storage-a5s63braj5xj2.storageaccount.queue.core.windows.net' -[Errno -2] Name or service not known -``` - -**Symptoms:** - -- Postprovision hook fails when trying to create App Configuration keys -- DNS resolution errors for Storage Account queue endpoint -- `azd env get-value STORAGE_ACCOUNT_NAME` returns incorrect value: `storage-a5s63braj5xj2.storageAccount` -- Expected value should be: `sta5s63braj5xj2` -- The `.storageAccount` suffix is the **module deployment name**, not the actual storage account name - ---- - -**Root Cause:** - -In `infra/bicep/modules/storage.bicep`, the storage account is deployed using the Azure Verified Module (AVM): - -```bicep -module storageAccount 'br/public:avm/res/storage/storage-account:0.27.1' = { - name: '${deployment().name}.storageAccount' // Deployment name: "storage-xyz.storageAccount" - params: { - name: storageAccountName // Actual resource name: "sta5s63braj5xj2" - } -} -``` - -The bug was in the module's output: - -```bicep -output name string = storageAccount.name // ❌ Returns deployment name -``` - -Bicep's `module.name` property returns the **deployment name** (the `name:` parameter of the module declaration), NOT the name of the resource created by that module. To get the actual resource name, you must access the module's outputs. - ---- - -**Impact:** - -1. **`main.bicep` output is incorrect:** - ```bicep - output STORAGE_ACCOUNT_NAME string = storage.outputs.name // Gets wrong value - ``` - - This output feeds `azd` environment variables - - Results in: `STORAGE_ACCOUNT_NAME=storage-a5s63braj5xj2.storageAccount` - -2. **Postprovision script fails (AILZ mode):** - ```bash - STORAGE_ACCOUNT=$(azd env get-value STORAGE_ACCOUNT_NAME) - # STORAGE_ACCOUNT="storage-a5s63braj5xj2.storageAccount" ❌ - - az appconfig kv set --name "$APP_CONFIG_NAME" \ - --key "contentflow.common.BLOB_STORAGE_ACCOUNT_NAME" \ - --value "$STORAGE_ACCOUNT" # Wrong value inserted! ❌ - ``` - -3. **DNS resolution fails:** - - Code constructs: `${STORAGE_ACCOUNT}.queue.core.windows.net` - - Results in: `storage-a5s63braj5xj2.storageaccount.queue.core.windows.net` ❌ - - Correct would be: `sta5s63braj5xj2.queue.core.windows.net` ✅ - -4. **Container Apps cannot access storage:** - - Apps read storage account name from App Configuration - - Receive incorrect value with `.storageAccount` suffix - - Cannot connect to blob storage or queues - ---- - -**Solution:** - -### File Changed: `infra/bicep/modules/storage.bicep` - -**Line 184 - Before:** -```bicep -output name string = storageAccount.name -``` - -**Line 184 - After:** -```bicep -output name string = storageAccount.outputs.name -``` - -**Explanation:** - -- `storageAccount.name` → Returns the **module deployment name** (`storage-xyz.storageAccount`) -- `storageAccount.outputs.name` → Returns the actual **storage account resource name** from the AVM module (`sta5s63braj5xj2`) - -The AVM module exposes the actual resource name via its output properties. By accessing `outputs.name`, we get the correct value that was passed in the `params.name` and used to create the actual Azure Storage Account resource. - ---- - -**Verification:** - -To verify the fix works correctly: - -```bash -# 1. Check azd environment has correct storage account name -azd env get-value STORAGE_ACCOUNT_NAME -# Expected: sta5s63braj5xj2 (no .storageAccount suffix) - -# 2. Verify DNS resolution works from jumpbox -nslookup sta5s63braj5xj2.queue.core.windows.net -# Should resolve to private IP (10.0.0.x) in AILZ mode - -# 3. Check App Configuration keys have correct value -az appconfig kv show \ - --name appcs- \ - --key "contentflow.common.BLOB_STORAGE_ACCOUNT_NAME" \ - --auth-mode login \ - --query "value" -o tsv -# Expected: sta5s63braj5xj2 - -# 4. Test storage queue creation from postprovision hook -az storage queue create \ - --name test-queue \ - --account-name sta5s63braj5xj2 \ - --auth-mode login -# Should succeed without DNS errors -``` - ---- - -**Related Modules (No Issues Found):** - -Audited all other modules that use the same pattern. All others correctly use `module.outputs.name`: - -✅ `log-analytics-ws.bicep`: `output name string = logAnalytics.outputs.name` -✅ `container-apps-environment.bicep`: `output name string = containerAppsEnvironment.outputs.name` -✅ `app-config-store.bicep`: `output name string = appConfigStore.outputs.name` -✅ `container-registry.bicep`: `output name string = containerRegistry.outputs.name` -✅ `user-assigned-identity.bicep`: `output name string = userAssignedIdentity.outputs.name` -✅ `static-web-app.bicep`: `output name string = staticWebApp.outputs.name` - -⚠️ `container-app.bicep`: Uses `output name string = containerApp.name` but this output is **never consumed** in `main.bicep` (only `.outputs.fqdn` is used), so no impact. - ---- - -**Benefits After Fix:** - -✅ **Correct storage account name** propagates to azd environment -✅ **DNS resolution works** for blob and queue endpoints -✅ **Postprovision hook succeeds** in AILZ mode -✅ **App Configuration keys** contain correct storage account name -✅ **Container Apps** can connect to storage successfully -✅ **No breaking changes** — existing code in `main.bicep` uses variable `storageAccountName` for App Config keys (basic mode), not affected by this output - ---- - -## 8. Fix: Queue Storage Private DNS Zone Missing in AILZ Resource Group - -**Error:** - -``` -LinkedInvalidPropertyId: Property id '' at path 'properties.privateDnsZoneConfigs[0].properties.privateDnsZoneId' -is invalid. Expect fully qualified resource Id that start with '/subscriptions/{subscriptionId}' -or '/providers/{resourceProviderNamespace}/'. -``` - -**Symptoms:** - -- Deployment fails after all resources provision successfully -- Error occurs during `storageQueueDnsZoneGroup` module creation -- `privateDnsZoneId` parameter is empty (`''`) -- Script `get-ailz-resources.sh` cannot find Queue Storage DNS Zone -- Azure Portal shows AILZ resource group (`rg-mini-ailz-jx05`) has only 6 Private DNS Zones: - - ✅ `privatelink.azconfig.io` - - ✅ `privatelink.azurecr.io` - - ✅ `privatelink.azurecontainerapps.io` - - ✅ `privatelink.blob.core.windows.net` - - ✅ `privatelink.cognitiveservices.azure.com` - - ✅ `privatelink.documents.azure.com` - - ❌ `privatelink.queue.core.windows.net` **(MISSING)** - ---- - -**Root Cause:** - -Azure Storage Account creates **two separate private endpoints** when network isolation is enabled: -1. **Blob endpoint** (`privatelink.blob.core.windows.net`) — ✅ Exists in AILZ RG -2. **Queue endpoint** (`privatelink.queue.core.windows.net`) — ❌ Does NOT exist in AILZ RG - -Both endpoints are created by ContentFlow deployment, but only the Blob DNS Zone was provisioned in the AILZ resource group. Without the Queue DNS Zone, the `storageQueueDnsZoneGroup` module cannot create the DNS zone group configuration, causing deployment failure. - -**Why This Wasn't Caught Earlier:** - -Items 1-7 focused on **existing** Private DNS Zones (ACR, Blob, Cosmos, App Config). The Queue DNS Zone requirement was assumed to exist alongside Blob, but AILZ infrastructure only provisioned Blob zone initially. - ---- - -**Solution: Conditional DNS Zone Group Creation** - -Since the Queue Storage Private DNS Zone is **infrastructure that must be created by the AILZ administrator** (not by this deployment template), we make the Queue DNS Zone Group creation **conditional** — it only executes if the DNS Zone exists. - -### Files Changed - -#### **Modified: `infra/bicep/main.bicep`** (line 247) - -**Before:** -```bicep -module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { - name: 'storage-queue-dns-zone-group-${resourceToken}' - params: { - privateEndpointName: '${storageAccountName}-queue-pe' - privateDnsZoneId: networkConfig.privateDnsZoneIds.queue - location: location - } - dependsOn: [storage] -} -``` - -**After:** -```bicep -module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated && !empty(networkConfig.privateDnsZoneIds.queue)) { - name: 'storage-queue-dns-zone-group-${resourceToken}' - params: { - privateEndpointName: '${storageAccountName}-queue-pe' - privateDnsZoneId: networkConfig.privateDnsZoneIds.queue - location: location - } - dependsOn: [storage] -} -``` - -**Explanation:** - -Added `&& !empty(networkConfig.privateDnsZoneIds.queue)` condition: -- **Before:** Module always executes in AILZ mode (even with empty DNS zone ID → deployment fails) -- **After:** Module only executes if DNS zone ID is not empty (deployment succeeds, DNS zone group skipped) - -This allows deployment to proceed without error while the Queue DNS Zone is missing, but maintains the correct configuration when the zone exists. - -#### **Modified: `infra/bicep/main.bicep`** (parameter added at line ~44) - -**Added:** -```bicep -@description('Resource ID of existing Queue Storage Private DNS Zone (required for ailz-integrated mode)') -param existingQueuePrivateDnsZoneId string = '' -``` - -#### **Modified: `infra/bicep/main.bicep`** (networkConfig object at line ~136) - -**Added:** -```bicep -privateDnsZoneIds: { - cognitiveServices: existingCognitiveServicesPrivateDnsZoneId - blob: existingBlobPrivateDnsZoneId - queue: existingQueuePrivateDnsZoneId // ← Added - cosmos: existingCosmosPrivateDnsZoneId - appConfig: existingAppConfigPrivateDnsZoneId - acr: existingAcrPrivateDnsZoneId - keyVault: existingKeyVaultPrivateDnsZoneId - containerAppsEnv: existingContainerAppsEnvPrivateDnsZoneId -} -``` - -#### **Modified: `infra/scripts/get-ailz-resources.sh`** (line ~180) - -**Added:** -```bash -get_resource_id "Queue Storage Private DNS Zone" "EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID" \ - "az network private-dns zone show --name privatelink.queue.core.windows.net --resource-group $AILZ_RG --query id -o tsv" -``` - ---- - -**Impact: Deployment Will Succeed BUT Queue Access Will Fail** - -With this fix: - -✅ **Deployment completes successfully** (no more `LinkedInvalidPropertyId` error) -✅ **All resources provision** (Storage Account, Container Apps, etc.) -✅ **Blob endpoint works** (has DNS zone + DNS zone group) -❌ **Queue endpoint DOES NOT WORK** (no DNS resolution) -❌ **Worker service cannot access queue** (cannot process content) - -**Why Queue Access Fails Without DNS Zone:** - -``` -Worker Container App (in VNet) - ↓ -Tries to connect: sta5s63braj5xj2.queue.core.windows.net - ↓ -DNS resolution fails (no A record in Private DNS Zone) - ↓ -Falls back to public DNS → gets public IP - ↓ -Storage Account has publicNetworkAccess: 'Disabled' - ↓ -Connection refused → HTTPSConnectionError -``` - -Without the DNS zone group, there's no A record registered in Private DNS Zone. DNS resolution fails, and the Worker cannot access the queue endpoint even though the private endpoint exists. - ---- - -**Required Action: Request DNS Zone Creation from AILZ Administrator** - -To fully resolve this issue, the AILZ administrator must create the missing Private DNS Zone. - -### Request Context for Administrator - -**What's Needed:** -- Create Private DNS Zone: `privatelink.queue.core.windows.net` -- Location: AILZ Resource Group `rg-mini-ailz-jx05` -- Link to VNet: Same VNet used for other Private DNS Zones -- Reason: Storage Account creates 2 private endpoints (blob + queue), each requires its own DNS zone - -**Technical Context:** - -Azure Storage Account with private endpoints enabled creates **two separate service endpoints**: -1. **Blob Service** (`*.blob.core.windows.net`) — DNS zone exists ✅ -2. **Queue Service** (`*.queue.core.windows.net`) — DNS zone missing ❌ - -Without both DNS zones, ContentFlow Worker service cannot access Azure Storage Queue, which is required for asynchronous content processing tasks. - -**Impact:** -- **Current:** Deployment succeeds but Worker service non-functional (cannot process content) -- **After DNS Zone Created:** Worker can access queue → full functionality restored - -### Azure CLI Commands for Administrator - -```bash -# 1. Create Queue Storage Private DNS Zone -az network private-dns zone create \ - --resource-group rg-mini-ailz-jx05 \ - --name privatelink.queue.core.windows.net - -# 2. Link DNS Zone to VNet (replace with actual VNet name) -az network private-dns link vnet create \ - --resource-group rg-mini-ailz-jx05 \ - --zone-name privatelink.queue.core.windows.net \ - --name ailz-vnet-link \ - --virtual-network \ - --registration-enabled false -``` - -**After DNS Zone Created:** - -1. Development team re-runs discovery script on jumpbox: - ```bash - cd ~/contentflow - ./infra/scripts/get-ailz-resources.sh - ``` - -2. Verify Queue DNS Zone ID is set: - ```bash - azd env get-value EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID - # Should return: /subscriptions/.../privateDnsZones/privatelink.queue.core.windows.net - ``` - -3. Re-deploy to create DNS zone group: - ```bash - azd provision --no-prompt - ``` - -4. Verify DNS zone group created: - ```bash - az network private-endpoint dns-zone-group list \ - --resource-group rg-contentflow-test004-jx05 \ - --endpoint-name sta5s63braj5xj2-queue-pe - ``` - -5. Verify A record registered: - ```bash - az network private-dns record-set a list \ - --resource-group rg-mini-ailz-jx05 \ - --zone-name privatelink.queue.core.windows.net - # Should show A record for sta5s63braj5xj2 → 10.0.0.x - ``` - -6. Test DNS resolution from jumpbox: - ```bash - nslookup sta5s63braj5xj2.queue.core.windows.net - # Should resolve to private IP (10.0.0.x) - ``` - ---- - -**Why Not Make Queue Storage Optional?** - -The Worker service requires Azure Storage Queue for task distribution and content processing orchestration. Without queue access, the core functionality of ContentFlow does not work. - -**Alternatives Considered:** -- ❌ **Enable public network access** — Violates Zero Trust / network isolation policies -- ❌ **Use only blob storage** — Requires major application architecture changes -- ❌ **Remove worker service** — ContentFlow becomes non-functional (cannot process content) -- ✅ **Request DNS Zone creation** — Minimal infrastructure change, preserves security, enables full functionality - ---- - -**Expected Results After DNS Zone Created and Redeployed:** - -✅ All 5 DNS zone groups exist (ACR, Blob, **Queue**, Cosmos, App Config) -✅ All 5 Private DNS Zones have A records registered -✅ Worker service can access Storage Queue via private endpoint -✅ Content processing workflows function correctly -✅ Complete end-to-end deployment successful - ---- - -**Verification After Full Fix:** - -```bash -# 1. Verify all 5 DNS zone groups exist -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -blob-pe -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -queue-pe # ← This one -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe -az network private-endpoint dns-zone-group list --resource-group --endpoint-name -pe - -# 2. Verify A records in all 5 Private DNS Zones -az network private-dns record-set a list --resource-group --zone-name privatelink.azurecr.io -az network private-dns record-set a list --resource-group --zone-name privatelink.blob.core.windows.net -az network private-dns record-set a list --resource-group --zone-name privatelink.queue.core.windows.net # ← This one -az network private-dns record-set a list --resource-group --zone-name privatelink.documents.azure.com -az network private-dns record-set a list --resource-group --zone-name privatelink.azconfig.io - -# 3. Test worker queue access (check Container App logs) -az containerapp logs show \ - --name worker- \ - --resource-group \ - --follow \ - --query "[?contains(Log, 'Queue')]" -``` - ---- - -## Solution Dependencies - -Some fixes depend on others to function correctly. Understanding these dependencies is important for deployment sequencing and troubleshooting. - -### Dependency Graph - -``` -Items 1-4 (Independent) - ↓ -Item 5: DNS Zone Groups (Foundation) - ↓ - ├──→ Item 6: App Config Keys Postprovision Hook (Requires DNS resolution) - ├──→ Item 7: Storage Account Name Output (Independent bug, but output consumed by Item 6) - └──→ Item 8: Queue Storage DNS Zone (Infrastructure prerequisite - BLOCKED) -``` - -### Dependency Details - -#### **Item 5 enables Item 6** (Critical Dependency) - -**Item 5** (DNS Zone Groups) is a **prerequisite** for **Item 6** (App Config Keys Postprovision Hook): - -- **Without Item 5:** Postprovision hook fails with DNS resolution errors - ``` - Failed to resolve 'sta5s63braj5xj2.queue.core.windows.net' - Failed to resolve 'appcs-a5s63braj5xj2.azconfig.io' - ``` - -- **With Item 5:** DNS zone groups register A records → jumpbox can resolve FQDNs → postprovision hook succeeds - -**Why:** The postprovision hook runs from the jumpbox VM (inside VNet) and must access Storage Account and App Configuration via private endpoints. Without DNS zone groups, there are no A records in Private DNS Zones, so DNS resolution fails. - -#### **Item 7 consumed by Item 6** (Data Dependency) - -**Item 7** (Storage Account Name Output) is **independent** but its output is **consumed by Item 6**: - -- **Without Item 7:** Wrong storage account name (`storage-xyz.storageAccount`) inserted into App Config -- **With Item 7:** Correct storage account name (`sta5s63braj5xj2`) inserted into App Config - -**Why:** The postprovision hook reads `STORAGE_ACCOUNT_NAME` from azd environment (populated from Bicep output) and writes it to App Configuration keys. Container Apps read this value to connect to blob storage and queues. - -#### **Item 8 blocks Worker functionality** (Infrastructure Dependency) - -**Item 8** (Queue Storage DNS Zone) is **BLOCKED** by missing infrastructure in AILZ resource group: - -- **Without Item 8:** Deployment succeeds but Worker service cannot access Storage Queue (no DNS resolution) -- **With Item 8:** Worker can access queue → full ContentFlow functionality - -**Why:** Storage Account creates two private endpoints (blob + queue). Item 5 implemented support for both DNS zone groups, but the Queue DNS Zone itself does not exist in AILZ RG. This is an **infrastructure prerequisite** that must be created by AILZ administrator before Worker service can function. - -**Current Status:** Code is ready (Items 5 + 8 completed), awaiting DNS zone creation by admin. - -#### **Items 1-4 are Independent** - -These fixes have no dependencies on other items: - -- **Item 1** (LogAnalytics CustomerId): Standalone Bicep reference() function fix -- **Item 2** (Property Name Typo): Simple typo fix in parameter name -- **Item 3** (UnmatchedPrincipalType): Standalone deployer() principalType detection -- **Item 4** (ACR Configuration Simplification): Architectural cleanup, no dependencies - -### Deployment Order (Recommended) - -All items are implemented in Bicep code and deployed together via `azd provision`. The dependency resolution happens automatically: - -1. **Bicep Provisioning** deploys Items 1-8 (in dependency order managed by Bicep's `dependsOn`) -2. **Item 5 modules execute** after resource modules (explicit `dependsOn: [storage]`, `dependsOn: [appConfigStore]`, etc.) -3. **Item 8 Queue DNS Zone Group** conditionally skipped (DNS zone doesn't exist yet) -4. **Postprovision Hook** (Item 6) executes after Bicep completes, relies on Item 5 DNS + Item 7 outputs - -**No manual sequencing needed** — the Bicep module dependencies and azd hooks ensure correct execution order. - -**Note:** Item 8 (Queue Storage DNS Zone) requires AILZ administrator action before full deployment can succeed. Current deployment completes but Worker service remains non-functional until Queue DNS Zone is created. From 0f2dc91f9c28f5a89a4dd4eba20a353e55ab8350 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Wed, 25 Mar 2026 09:43:05 -0400 Subject: [PATCH 20/29] 00-full-analysis.md --- Analysis/00-full-analysis.md | 1129 ++++++++++++++++++++++++++++++++++ Untitled.md | 0 2 files changed, 1129 insertions(+) create mode 100644 Analysis/00-full-analysis.md create mode 100644 Untitled.md diff --git a/Analysis/00-full-analysis.md b/Analysis/00-full-analysis.md new file mode 100644 index 0000000..37e81e8 --- /dev/null +++ b/Analysis/00-full-analysis.md @@ -0,0 +1,1129 @@ +# ContentFlow — Complete Reference Analysis + +> **Single-file reference document for AI agents and developers.** +> This document consolidates all capabilities, architecture, prerequisites, out-of-scope boundaries, and example scenarios for the ContentFlow accelerator. Use it as the authoritative context source before working with this codebase. + +--- + +## Table of Contents + +1. [What is ContentFlow?](#1-what-is-contentflow) +2. [Core Concepts](#2-core-concepts) +3. [Full Capabilities](#3-full-capabilities) + - [3.1 Built-in Executors (37 total)](#31-built-in-executors-37-total) + - [3.2 Pipeline Engine Features](#32-pipeline-engine-features) + - [3.3 Web UI Features](#33-web-ui-features) + - [3.4 API Features](#34-api-features) + - [3.5 Worker Engine Features](#35-worker-engine-features) + - [3.6 Extensibility: Custom Executors](#36-extensibility-custom-executors) +4. [Prerequisites](#4-prerequisites) + - [4.1 Azure Services Required](#41-azure-services-required) + - [4.2 Developer Toolchain](#42-developer-toolchain) + - [4.3 Azure Permissions](#43-azure-permissions) + - [4.4 Service Quotas & Limits](#44-service-quotas--limits) +5. [Final Architecture](#5-final-architecture) + - [5.1 Component Overview](#51-component-overview) + - [5.2 Service Communication Map](#52-service-communication-map) + - [5.3 Data Flow](#53-data-flow) + - [5.4 Task Lifecycle](#54-task-lifecycle) + - [5.5 Cosmos DB Schema](#55-cosmos-db-schema) + - [5.6 Deployment Modes](#56-deployment-modes) + - [5.7 Security Model](#57-security-model) +6. [Out of Scope](#6-out-of-scope) +7. [Example Scenarios](#7-example-scenarios) + - [7.1 Invoice & Receipt Processing](#71-invoice--receipt-processing) + - [7.2 Enterprise Knowledge Base for RAG](#72-enterprise-knowledge-base-for-rag) + - [7.3 Contract Analysis](#73-contract-analysis) + - [7.4 Engineering PDF BOM Extraction](#74-engineering-pdf-bom-extraction) + - [7.5 Compliance & PII Detection](#75-compliance--pii-detection) + - [7.6 Web Content Ingestion](#76-web-content-ingestion) + - [7.7 Multilingual Document Translation](#77-multilingual-document-translation) + - [7.8 Email Attachment Triage](#78-email-attachment-triage) + - [7.9 Spreadsheet Data Pipeline](#79-spreadsheet-data-pipeline) + - [7.10 Knowledge Graph Construction](#710-knowledge-graph-construction) +8. [Included Sample Pipelines](#8-included-sample-pipelines) + +--- + +## 1. What is ContentFlow? + +**ContentFlow** is an open-source, enterprise-grade, cloud-native document and content processing accelerator built on Azure. It transforms unstructured content — PDFs, Word documents, Excel files, PowerPoint presentations, CSV files, web pages, images — into structured, intelligent, actionable data through orchestrated AI-powered pipelines. + +**Primary value proposition**: ContentFlow eliminates the infrastructure plumbing required to build scalable document processing systems on Azure. A developer can go from zero to a production-grade pipeline in hours rather than weeks by combining pre-built executors declaratively via YAML. + +**Repository**: [Azure/contentflow](https://github.com/Azure/contentflow) +**License**: MIT +**Runtime**: Python 3.12+ (backend), TypeScript/React 18 (frontend) + +--- + +## 2. Core Concepts + +### Pipeline +A **pipeline** is the fundamental unit of work. It defines an ordered sequence of executors that content passes through. Pipelines are declared in YAML and stored in Cosmos DB. + +```yaml +name: my-pipeline +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documents + - executor: azure_document_intelligence_extractor + settings: + doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" + - executor: recursive_text_chunker + settings: + chunk_size: 1000 + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + - executor: ai_search_index_output + settings: + index_name: my-index +``` + +### Executor +An **executor** is a self-contained processing unit. Each executor receives a `Content` object, transforms or enriches it, and passes the result to the next step. Executors are composable, configurable via YAML settings, and can run in parallel. + +### Vault +A **vault** is a logical container that associates a document source with a pipeline. When enabled, the Worker service automatically discovers new documents arriving in the vault and queues them for processing. + +### Content Model +The standardized Python object (`Content`) that flows through every pipeline step: + +```python +Content + ├── id: ContentIdentifier # canonical_id, source_type, container, path, filename + ├── data: dict # All executor outputs accumulate here + ├── summary_data: dict # Execution metrics per executor + └── executor_logs: list # Per-step audit trail +``` + +All executor outputs are stored as named fields in `content.data`, enabling downstream executors to reference previous results via dot-notation paths (e.g., `doc_intell_output.tables`). + +--- + +## 3. Full Capabilities + +### 3.1 Built-in Executors (37 total) + +#### Input Executors (3) +| Executor ID | Description | +|---|---| +| `azure_blob_input_discovery` | Discovers files in Azure Blob Storage containers. Supports file extension filtering, prefix filtering, and incremental crawling via checkpoints (processes only new/modified blobs since last run). | +| `azure_blob_content_retriever` | Downloads blob content as bytes or saves to a temp file path for downstream processing. Supports parallel retrieval with configurable concurrency. | +| `content_retriever` | Loads content from local file system or arbitrary byte sources. Primarily used in local development and sample pipelines. | + +#### Document Extraction Executors (6) +| Executor ID | Description | +|---|---| +| `azure_document_intelligence_extractor` | Invokes Azure Document Intelligence (prebuilt-layout, prebuilt-document, prebuilt-read) to extract text, tables, key-value pairs, and figures from any document. Supports markdown or plain text output format, page range selection, locale hints, and additional features like `ocrHighResolution`. | +| `azure_content_understanding_extractor` | Uses Azure Content Understanding for advanced document analysis including prebuilt invoice, receipt, business card, and ID models. | +| `pdf_extractor` | Extracts text, page-level chunks, and embedded images from PDF files using PyMuPDF. No Azure service dependency — runs locally. | +| `word_extractor` | Extracts text, headings, tables, and metadata from `.docx` files using python-docx. | +| `powerpoint_extractor` | Extracts slide content, speaker notes, embedded images, and metadata from `.pptx` files using python-pptx. | +| `excel_extractor` | Extracts sheets, tables, cell values, formulas, images, and metadata from `.xlsx`/`.xlsm` files using openpyxl. Supports first-row-as-header detection, sheet filtering, and table range detection. | +| `csv_extractor` | Parses CSV files with configurable delimiter, quoting, and encoding. | + +#### AI Processing Executors (8) +| Executor ID | Description | +|---|---| +| `azure_openai_agent` | Sends content to Azure OpenAI (GPT-4.1, GPT-4.1-mini, etc.) with a configurable system prompt and user template. The most flexible executor — suitable for summarization, extraction, classification, transformation, and any custom reasoning task. | +| `azure_openai_embeddings` | Generates vector embeddings using Azure OpenAI embedding models (text-embedding-3-small, text-embedding-3-large). Output format compatible with Azure AI Search vector fields. | +| `text_summarizer` | Generates concise summaries with configurable max length and style. Built on `azure_openai_agent`. | +| `entity_extractor` | Extracts named entities (persons, organizations, dates, amounts, locations) from text. Configurable entity types. Built on `azure_openai_agent`. | +| `sentiment_analyser` | Document-level, sentence-level, or aspect-based sentiment analysis with confidence scores. Supports 3-point and 5-point scales and emotion detection. | +| `content_classifier` | Multi-class and multi-label classification with confidence scores and explanations. Accepts custom category lists and descriptions. | +| `pii_detector` | Detects and optionally redacts Personally Identifiable Information (PII) such as names, emails, phone numbers, SSNs, credit cards, addresses. | +| `keyword_extractor` | Extracts top-N keywords and key phrases from text content. | + +#### Transformation Executors (6) +| Executor ID | Description | +|---|---| +| `recursive_text_chunker` | Splits long text into overlapping chunks using recursive character splitting with configurable chunk size, overlap, and separators. | +| `table_row_splitter` | Splits tabular data (list-of-lists, list-of-dicts, CSV string, Word tables, Excel rows) into individual `Content` objects — one per row — enabling per-row parallel processing downstream. | +| `field_mapper` | Renames, remaps, and transforms fields within `content.data` using a declarative mapping configuration. | +| `field_selector` | Includes or excludes specific fields from `content.data` to control what data is passed forward. | +| `language_detector` | Detects the primary language of text content. | +| `content_translator` | Translates content to one or more target languages using Azure OpenAI. | + +#### Output Executors (3) +| Executor ID | Description | +|---|---| +| `azure_blob_output` | Writes processed content (JSON, text, binary) back to Azure Blob Storage. Supports custom path templates and content serialization. | +| `ai_search_index_output` | Indexes processed content into Azure AI Search. Supports vector fields, semantic configuration, and batch indexing. | +| `gptrag_search_index_document_generator` | Generates documents in GPT-RAG-compatible format for Azure AI Search, including chunked content and embeddings for RAG scenarios. | + +#### Control Flow Executors (5) +| Executor ID | Description | +|---|---| +| `subpipeline` | Executes another pipeline as a nested sub-workflow. Enables pipeline composition and reuse. | +| `for_each_content` | Iterates over a list of content items and applies a sub-pipeline to each one. | +| `fan_in_aggregator` | Collects and merges results from parallel branches back into a single content stream. | +| `document_set_initializer` | Initializes a multi-document analysis session for cross-document operations. | +| `document_set_collector` | Collects documents into a set for batch cross-document processing. | + +#### Cross-Document Executors (3) +| Executor ID | Description | +|---|---| +| `cross_document_comparison` | Compares multiple documents side-by-side using AI to identify similarities, differences, and patterns. | +| `cross_document_field_aggregator` | Aggregates specific fields across a document set (e.g., collect all "summary" fields from 50 documents). | +| `cosmos_db_lookup` | Looks up metadata or previously stored results from Cosmos DB during pipeline execution. Useful for enrichment and deduplication. | + +#### Utility Executors (3) +| Executor ID | Description | +|---|---| +| `pass_through` | No-op executor. Useful for pipeline testing and debugging. | +| `web_scraper` | Crawls web pages using Playwright (headless Chromium). Supports CSS selectors, depth control, and JavaScript-rendered pages. | +| `cosmos_db_lookup` | Retrieves enrichment data from Cosmos DB based on a key field in the content. | + +--- + +### 3.2 Pipeline Engine Features + +- **Declarative YAML definition** — pipelines are stored as YAML and parsed at runtime +- **DAG execution** — pipelines can define explicit `execution_sequence` for dependency control +- **Async/await throughout** — all executors are async; built on Python asyncio +- **Parallel execution** — `ParallelExecutor` base class enables `max_concurrent` processing (default: 3 concurrent items) +- **Conditional execution** — any executor supports a `condition` setting (Python expression evaluated against `content.data`) +- **Error handling** — `fail_pipeline_on_error` and `continue_on_error` per executor; up to 3 retries with configurable delay +- **Timeout control** — per-executor `timeout_secs` setting +- **Debug mode** — verbose logging per executor via `debug_mode: true` +- **Nested fields** — all settings support dot-notation for accessing nested data (e.g., `doc_intell_output.tables`) +- **Environment variable substitution** — any setting value can reference env vars with `${VAR_NAME}` syntax +- **Sub-pipelines** — pipelines can invoke other pipelines as steps, enabling workflow composition +- **Parallel fan-out/fan-in** — native support for splitting content into parallel branches and merging results +- **Incremental processing** — vault crawl checkpoints prevent duplicate reprocessing + +--- + +### 3.3 Web UI Features + +**Technology**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn/ui, Radix UI, ReactFlow, Monaco Editor + +| Feature | Description | +|---|---| +| **Visual Pipeline Builder** | Drag-and-drop graphical interface built on ReactFlow. Nodes represent executors; edges represent data flow. | +| **YAML Editor** | Monaco-powered code editor with syntax highlighting for editing pipeline definitions directly. | +| **Template Gallery** | Pre-built pipeline templates for common scenarios, ready to clone and customize. | +| **Vault Management** | Create, edit, enable/disable document vaults. Associate vaults with pipelines. | +| **Execution Dashboard** | Real-time view of pipeline execution status, metrics, and per-executor output. | +| **Executor Catalog Browser** | Browse all 37 executors with descriptions, settings schemas, and usage examples. | +| **Knowledge Graph Viewer** | Visualization of entity-relationship graphs produced by knowledge graph pipelines. | + +**Routes**: +- `/` — Home dashboard +- `/?view=pipeline` — Pipeline builder +- `/?view=vaults` — Vault management +- `/?view=graph` — Knowledge graph visualization +- `/templates` — Template gallery + +--- + +### 3.4 API Features + +**Technology**: FastAPI 0.128, Python 3.12+, Uvicorn, Pydantic v2, Azure SDK +**Port**: 8090 +**Authentication**: Azure Managed Identity (no API keys in production) + +| Endpoint group | Operations | +|---|---| +| `GET /api/health` | System health check — verifies Cosmos DB, Blob, Queue, App Config connectivity | +| `GET/POST/PUT/DELETE /api/pipelines` | Full CRUD for pipeline definitions; trigger manual execution | +| `GET/POST/PUT/DELETE /api/vaults` | Vault management including enable/disable | +| `GET /api/vaults/{id}/executions` | Vault execution history and status | +| `GET /api/executors` | Browse executor catalog with full settings schemas | +| `GET /docs` | Interactive Swagger UI | +| `GET /redoc` | ReDoc documentation | + +--- + +### 3.5 Worker Engine Features + +**Technology**: Python 3.12+, multiprocessing, Azure Storage Queue +**Port**: 8099 (health API) + +| Feature | Description | +|---|---| +| **Multi-process architecture** | N Source Workers + M Processing Workers run as independent OS processes | +| **Source Workers** | Poll enabled vaults every 5 minutes (configurable), discover new content, enqueue tasks | +| **Processing Workers** | Poll Azure Storage Queue, execute full pipelines on content items | +| **Incremental crawling** | Checkpoints stored in Cosmos DB prevent reprocessing already-seen documents | +| **Distributed locking** | Lock TTL prevents duplicate processing when multiple workers poll simultaneously | +| **Auto-restart** | Monitor loop detects crashed worker processes and replaces them automatically | +| **Graceful shutdown** | SIGINT/SIGTERM handled via shared `multiprocessing.Event`; workers finish current task before stopping | +| **Configurable parallelism** | `NUM_PROCESSING_WORKERS` and `NUM_SOURCE_WORKERS` env vars | +| **Task retries** | Up to 3 retries per task with configurable delay | + +--- + +### 3.6 Extensibility: Custom Executors + +ContentFlow is designed for extension. New executors can be created by subclassing one of three base classes: + +| Base Class | Use case | +|---|---| +| `BaseExecutor` | Single content item or list processing; implement `process_input()` | +| `ParallelExecutor` | Automatic parallel processing; implement `process_content_item()` once; framework handles concurrency | +| `InputExecutor` | Input source discovery; implement `crawl()` to yield new content items | + +After implementing the class: +1. Register it in `executor_catalog.yaml` with its `id`, `module_path`, `class_name`, tags, and `settings_schema` +2. Import it in `contentflow/executors/__init__.py` + +Custom executors work identically to built-in ones — they appear in the Web UI, accept YAML configuration, support all base class features (debug mode, conditions, retries, timeouts), and integrate with the full pipeline engine. + +--- + +## 4. Prerequisites + +### 4.1 Azure Services Required + +| Service | SKU (Basic mode) | SKU (AILZ mode) | Purpose | +|---|---|---|---| +| **Azure Container Apps Environment** | Consumption | Consumption (Internal) | Hosts API, Worker, Web containers | +| **Azure Cosmos DB** | Serverless | Serverless + Private Endpoint | Pipeline definitions, execution state, vault metadata | +| **Azure Blob Storage** | Standard LRS, Hot | Standard LRS + Private Endpoint | Document storage, processed outputs | +| **Azure Storage Queue** | Standard | Standard + Private Endpoint | Task queue between Source and Processing Workers | +| **Azure App Configuration** | Standard | Standard + Private Endpoint | Centralized runtime configuration | +| **Azure Application Insights** | Pay-per-use | Pay-per-use | Distributed tracing, monitoring | +| **Azure Log Analytics** | PerGB2018 | PerGB2018 (shared) | Log aggregation | +| **Azure Container Registry** | Standard | **Premium** (required for Private Endpoint) | Container image registry | +| **Azure Managed Identity** | User-assigned | User-assigned | Passwordless authentication between all services | + +**Optional Azure services** (required for specific executors): + +| Service | Required by executor(s) | +|---|---| +| **Azure AI Foundry / OpenAI** | `azure_openai_agent`, `azure_openai_embeddings`, `text_summarizer`, `entity_extractor`, `sentiment_analyser`, `content_classifier`, `pii_detector`, `keyword_extractor`, `content_translator` | +| **Azure Document Intelligence** | `azure_document_intelligence_extractor` | +| **Azure Content Understanding** | `azure_content_understanding_extractor` | +| **Azure AI Search** | `ai_search_index_output`, `gptrag_search_index_document_generator` | + +### 4.2 Developer Toolchain + +```bash +# Required for local development +python --version # 3.12+ +node --version # 18+ +npm --version # 9+ +docker --version # Any recent version +az --version # Azure CLI 2.60+ +azd version # Azure Developer CLI 1.5+ + +# Required for deployment +git --version # Any recent version +``` + +**Python dependencies** (installed from `requirements.txt` per service): +- `fastapi>=0.128.0`, `uvicorn>=0.40.0`, `pydantic>=2.12` +- `azure-identity>=1.25.1`, `azure-cosmos>=4.14.3` +- `azure-storage-blob>=12.27.1`, `azure-storage-queue>=12.14.1` +- `azure-appconfiguration-provider>=2.3.1` +- `azure-ai-documentintelligence` (for Document Intelligence) +- `openai` (for Azure OpenAI) +- `pymupdf` (for PDF extraction) +- `python-docx`, `python-pptx`, `openpyxl` (Office documents) +- `playwright` (for web scraping — requires browser install) +- `agent-framework` (Microsoft Agent Framework — pipeline orchestration) + +### 4.3 Azure Permissions + +| Permission | Scope | Required for | +|---|---|---| +| `Contributor` | ContentFlow Resource Group | Deploy all resources | +| `User Access Administrator` | ContentFlow Resource Group | Assign RBAC to Managed Identity | +| `Network Contributor` | VNet Resource Group | Create Private Endpoints (AILZ mode only) | +| `Private DNS Zone Contributor` | DNS Zone Resource Group | Register PE records (AILZ mode only) | + +**RBAC roles assigned automatically to the Managed Identity during deployment**: +- `Storage Blob Data Contributor` on Storage Account +- `Storage Queue Data Contributor` on Storage Account +- `Cosmos DB Built-in Data Contributor` on Cosmos DB +- `App Configuration Data Reader` on App Configuration +- `AcrPull` on Container Registry +- `Cognitive Services User` on AI Services (when applicable) + +### 4.4 Service Quotas & Limits + +| Limit | Default | Notes | +|---|---|---| +| Container Apps replicas | 1–2 per service | Auto-scales based on load | +| Worker processes | Configurable | `NUM_PROCESSING_WORKERS` env var | +| Queue message visibility timeout | 300 seconds | Task must complete within 5 minutes or is retried | +| Task max retries | 3 | Configurable via pipeline settings | +| Task timeout | 600 seconds | 10 minutes per pipeline execution | +| Cosmos DB throughput | Serverless (on-demand) | No pre-provisioned RU/s needed | +| Azure OpenAI TPM | Varies by deployment | Should be sized for expected concurrency | + +--- + +## 5. Final Architecture + +### 5.1 Component Overview + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ AZURE CONTAINER APPS ENVIRONMENT │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ +│ │ contentflow- │ │ contentflow- │ │ contentflow- │ │ +│ │ web │ │ api │ │ worker │ │ +│ │ │ │ │ │ │ │ +│ │ React 18 + │──▶│ FastAPI │ │ Multi-process │ │ +│ │ TypeScript │ │ Python 3.12+ │ │ Python 3.12+ │ │ +│ │ Port: 80 │ │ Port: 8090 │ │ Port: 8099 │ │ +│ │ │ │ │ │ (health only) │ │ +│ │ Pipeline │ │ Routers: │ │ │ │ +│ │ Builder UI │ │ /pipelines │ │ Source Workers │ │ +│ │ Vault Manager │ │ /vaults │ │ (crawl vaults) │ │ +│ │ Exec Dashboard │ │ /executors │ │ │ │ +│ │ Template Gall. │ │ /health │ │ Processing │ │ +│ └─────────────────┘ └────────┬────────┘ │ Workers │ │ +│ │ │ (run pipelines) │ │ +└──────────────────────────────────┼────────────┴──────────────────┘ │ + │ │ + ┌─────────────────────────┼────────────────────┼──────────────┐ + │ ▼ ▼ │ + │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ + │ │ Cosmos DB │ │ Blob Storage │ │ Storage Queue │ │ + │ │ (Serverless)│ │ (LRS, Hot) │ │ (processing │ │ + │ │ │ │ │ │ tasks) │ │ + │ │ pipelines │ │ vaults/ │ │ │ │ + │ │ vaults │ │ processed/ │ │ │ │ + │ │ executions │ │ outputs/ │ │ │ │ + │ │ checkpoints │ │ │ │ │ │ + │ └──────────────┘ └──────────────┘ └─────────────────┘ │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ + │ │ App Config │ │ App Insights│ │ Container │ │ + │ │ (Standard) │ │ + Log Anal. │ │ Registry │ │ + │ └──────────────┘ └──────────────┘ └─────────────────┘ │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ + │ │ AI Foundry │ │ Document │ │ Azure AI │ │ + │ │ (OpenAI) │ │ Intelligence│ │ Search │ │ + │ │ (optional) │ │ (optional) │ │ (optional) │ │ + │ └──────────────┘ └──────────────┘ └─────────────────┘ │ + │ AZURE MANAGED SERVICES │ + └──────────────────────────────────────────────────────────────┘ +``` + +### 5.2 Service Communication Map + +| From | To | Protocol | Auth | +|---|---|---|---| +| Web UI | ContentFlow API | HTTPS REST | None (same environment) | +| API | Cosmos DB | Azure SDK over HTTPS | Managed Identity | +| API | Blob Storage | Azure SDK over HTTPS | Managed Identity | +| API | Azure App Configuration | Azure SDK over HTTPS | Managed Identity | +| Worker (Source) | Cosmos DB | Azure SDK over HTTPS | Managed Identity | +| Worker (Source) | Storage Queue | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Storage Queue | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Cosmos DB | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Blob Storage | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Azure OpenAI | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Document Intelligence | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Azure AI Search | Azure SDK over HTTPS | Managed Identity | + +### 5.3 Data Flow + +``` +1. CONTENT DISCOVERY + Source Worker + │ + ├── Query Cosmos DB → enabled vaults + ├── For each vault → read associated pipeline + ├── Run input executor (e.g., azure_blob_input_discovery) + ├── Discover new files since last checkpoint + ├── Create ContentProcessingTask per file + ├── Enqueue tasks → Azure Storage Queue + └── Save new crawl checkpoint → Cosmos DB + +2. CONTENT PROCESSING + Processing Worker + │ + ├── Poll Storage Queue (up to 32 messages) + ├── Deserialize ContentProcessingTask + ├── Acquire distributed lock (prevent duplicate processing) + ├── Load pipeline YAML from Cosmos DB + ├── For each pipeline step (skipping input executors): + │ ├── Instantiate executor + │ ├── Check condition (if set) + │ ├── Call executor.process_input(content) + │ └── Accumulate results in content.data + ├── Update execution status → Cosmos DB (COMPLETED / FAILED) + ├── Write output → Blob Storage (if configured) + └── Delete message from queue + +3. OUTPUT + Configured output executors write to: + ├── Azure Blob Storage (JSON, text, binary) + ├── Azure AI Search (indexed documents) + └── Any custom destination via custom executor +``` + +### 5.4 Task Lifecycle + +``` +[PENDING] ← Task created and enqueued + │ + ▼ +[RUNNING] ← Processing Worker picks up the task + │ + ├──── Success ──→ [COMPLETED] + │ │ + │ Store results in Cosmos DB + Blob + │ + ├──── Failure ──→ [FAILED] + │ │ + │ ├── retry_count < max_retries (3)? + │ │ └── Yes → re-enqueue → [PENDING] + │ └── No → [FAILED] (permanent) + │ + └──── Cancelled ─→ [CANCELLED] +``` + +### 5.5 Cosmos DB Schema + +**Database**: `contentflow-db` + +| Container | Partition Key | Contents | +|---|---|---| +| `pipelines` | `/id` | Pipeline YAML definitions, graph nodes/edges, metadata | +| `vaults` | `/id` | Vault configurations, associated pipeline IDs | +| `vault_executions` | `/vault_id` | Per-document execution records with status and outputs | +| `pipeline_executions` | `/pipeline_id` | Direct pipeline execution records | +| `vault_crawl_checkpoints` | `/vault_id` | Last crawl timestamp per vault+executor | +| `executor_locks` | `/task_id` | Distributed locks for deduplication | +| `executor_catalog` | `/id` | Cached executor catalog (loaded from YAML at startup) | + +### 5.6 Deployment Modes + +#### Basic Mode (Public Endpoints) +All Azure resources expose public endpoints. Suitable for development, demos, and non-regulated environments. + +``` +Internet → Container Apps (external ingress) → Azure Services (public endpoints) +``` + +**Deploy**: `azd up` (one command) + +#### AILZ-Integrated Mode (Private Endpoints) +All Azure resources have `publicNetworkAccess: Disabled`. Traffic flows only through Private Endpoints inside a VNet. Suitable for enterprise, regulated environments, and AI Landing Zone integration. + +``` +JumpBox/VPN → VNet → Container Apps (internal LB) → Azure Services (Private Endpoints) +``` + +**Differences from Basic**: +- Container Apps Environment: `internal: true` (no public ingress) +- All services: `publicNetworkAccess: Disabled` +- Network ACLs: `defaultAction: Deny`, `bypass: AzureServices` +- Container Registry: must be **Premium** SKU (required for Private Endpoint support) +- Requires pre-existing VNet with subnets: `pe-subnet` and `aca-env-subnet` +- Requires 6 Private DNS Zones linked to the VNet: + - `privatelink.blob.core.windows.net` + - `privatelink.queue.core.windows.net` + - `privatelink.documents.azure.com` + - `privatelink.azconfig.io` + - `privatelink.azurecr.io` + - `privatelink.cognitiveservices.azure.com` + +**Deploy**: `azd up` with `ailzMode=ailz-integrated` parameter and pre-existing network resource IDs. + +### 5.7 Security Model + +| Principle | Implementation | +|---|---| +| **Zero-Trust** | No passwords, no connection strings. All service-to-service communication uses Managed Identity. | +| **Passwordless** | `ChainedTokenCredential` (Managed Identity → `DefaultAzureCredential`) used across all Azure SDK calls. | +| **Least Privilege** | Each service's Managed Identity is assigned only the RBAC roles it needs on each resource. | +| **Network Isolation** | AILZ mode enforces private networking; Basic mode exposes public endpoints with HTTPS. | +| **Secrets Management** | All secrets are either environment variables injected at deployment time or resolved from Azure App Configuration. No secrets in code or Git. | +| **Centralized Config** | Azure App Configuration is the single source of truth for runtime settings; local `.env` is for development only. | +| **CORS** | API configures `allow_origins: ["*"]` (suitable for internal deployments; restrict in production). | + +--- + +## 6. Out of Scope + +The following capabilities are **not included** in ContentFlow out-of-the-box and would require custom executor development, additional Azure services, or integration work: + +| Capability | Notes | +|---|---| +| **SharePoint read/write connector** | `ContentIdentifier.source_type` mentions "sharepoint" but no executor or connector exists. Would require Microsoft Graph API integration as a custom executor. | +| **OneDrive connector** | Same as SharePoint — no native executor. The Web UI shows it as a planned input type. | +| **Excel/XLSX writer** | `ExcelExtractorExecutor` reads Excel but there is no executor to **write** formatted `.xlsx` files (e.g., with conditional formatting). Requires custom executor using openpyxl. | +| **Confidence score per BOM cell (Document Intelligence)** | The `extract_tables()` method in `DocumentIntelligenceConnector` returns cell content and position but does not expose `cell.confidence` from the SDK response. Requires a code extension to the connector. | +| **Email (Exchange/Outlook) connector** | No native email source executor. Would require Microsoft Graph API integration. | +| **Relational database connector** (SQL, PostgreSQL) | No native DB input executor. Would require a custom `InputExecutor` subclass. | +| **Real-time / streaming processing** | ContentFlow is a batch/async pipeline system. It does not support real-time streaming ingestion (e.g., Kafka, Event Hub consumer). | +| **Chat interface** | No conversational UI or chat bot. ContentFlow processes documents, it does not serve a chat endpoint. Integration with Copilot Studio or Azure Bot Service is out of scope. | +| **Azure AI Search index schema management** | The `ai_search_index_output` executor writes to an existing index but does not create or manage index schemas. Index schema provisioning must happen separately. | +| **Custom Document Intelligence model training** | ContentFlow uses pre-built or previously trained models. Training custom Document Intelligence models is outside the accelerator's scope. | +| **Dynamics 365 / SAP integration** | No native connectors for line-of-business systems. Outputs to Blob or AI Search can be consumed by downstream integrations. | +| **Multi-tenant isolation** | All pipelines and vaults within a deployment share the same Cosmos DB and Blob Storage. There is no tenant-level data separation built in. | +| **Role-based access control in the UI** | The Web UI has no authentication or authorization layer. It is intended for internal use by platform operators. | +| **Built-in pipeline scheduling** | Vaults trigger continuous background polling; there is no cron-style scheduler. Time-based triggers must be implemented externally (e.g., Logic Apps, Azure Functions). | +| **Video or audio processing** | No native executors for audio transcription or video analysis. These would require Azure Speech or Azure Video Indexer custom executors. | + +--- + +## 7. Example Scenarios + +### 7.1 Invoice & Receipt Processing + +**Goal**: Extract structured data from incoming invoices PDFs and images; classify by document type; flag anomalies; output to Blob Storage. + +**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_content_understanding_extractor` (prebuilt-invoice) → `content_classifier` → `field_mapper` → `azure_blob_output` + +```yaml +name: invoice-processing +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: invoices-incoming + file_extensions: [".pdf", ".png", ".jpg"] + + - executor: azure_blob_content_retriever + + - executor: azure_content_understanding_extractor + settings: + model_id: prebuilt-invoice + output_field: invoice_data + + - executor: content_classifier + settings: + categories: ["invoice", "receipt", "purchase_order", "credit_note"] + include_confidence: true + + - executor: field_mapper + settings: + mappings: + invoice_data.VendorName: vendor_name + invoice_data.InvoiceTotal.amount: total_amount + invoice_data.InvoiceDate: invoice_date + + - executor: azure_blob_output + settings: + blob_container_name: invoices-processed + output_format: json +``` + +--- + +### 7.2 Enterprise Knowledge Base for RAG + +**Goal**: Process a mixed-format document library (PDF, Word, Excel), chunk text, generate embeddings, and index in Azure AI Search to power a RAG chatbot. + +**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_document_intelligence_extractor` → `recursive_text_chunker` → `azure_openai_embeddings` → `gptrag_search_index_document_generator` → `ai_search_index_output` + +```yaml +name: knowledge-base-ingestion +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: company-documents + file_extensions: [".pdf", ".docx", ".xlsx"] + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + settings: + doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" + model_id: prebuilt-layout + extract_text: true + extract_tables: true + + - executor: recursive_text_chunker + settings: + chunk_size: 800 + chunk_overlap: 150 + input_field: doc_intell_output.text + + - executor: azure_openai_embeddings + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: text-embedding-3-small + input_field: chunks + + - executor: gptrag_search_index_document_generator + settings: + index_name: company-knowledge-base + + - executor: ai_search_index_output + settings: + endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" + index_name: company-knowledge-base +``` + +--- + +### 7.3 Contract Analysis + +**Goal**: Extract obligations, parties, dates, monetary values, and risk clauses from legal contracts. Generate an executive summary per document. + +**Key executors**: `pdf_extractor` → `entity_extractor` → `azure_openai_agent` → `text_summarizer` → `azure_blob_output` + +```yaml +name: contract-analysis +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: contracts + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + settings: + extract_text: true + extract_pages: true + + - executor: entity_extractor + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + entity_types: ["person", "organization", "date", "monetary_amount", "jurisdiction"] + output_field: entities + + - executor: azure_openai_agent + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + instructions: | + You are a legal contract analyst. Analyze the contract and extract: + 1. Key obligations per party + 2. Payment terms and amounts + 3. Termination clauses + 4. Governing law and jurisdiction + 5. Risk flags (unusual or one-sided clauses) + Respond in structured JSON. + input_field: pdf_output.text + output_field: contract_analysis + + - executor: text_summarizer + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + max_length: 400 + style: executive summary for legal review + input_field: pdf_output.text + output_field: summary + + - executor: azure_blob_output + settings: + blob_container_name: contracts-analyzed +``` + +--- + +### 7.4 Engineering PDF BOM Extraction + +**Goal**: Extract Bill of Materials (BOM) tables from engineering PDFs using Azure Document Intelligence, flag low-confidence line items (<90%), and write output to Azure Blob Storage. *(See fit assessment in previous analysis for implementation gap details.)* + +**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_document_intelligence_extractor` → `table_row_splitter` → `field_mapper` → [custom: `bom_excel_writer`] → [custom: `sharepoint_output`] + +```yaml +name: engineering-bom-extraction +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: engineering-pdfs + file_extensions: [".pdf"] + + - executor: azure_blob_content_retriever + settings: + use_temp_file_for_content: true + + - executor: azure_document_intelligence_extractor + settings: + doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" + model_id: prebuilt-layout + extract_tables: true + extract_text: false + output_field: doc_intell_output + + - executor: table_row_splitter + settings: + table_field: doc_intell_output.tables.0 + table_format: auto + has_header: true + skip_empty_rows: true + include_row_index: true + + - executor: field_mapper + settings: + mappings: + row_data.Item: item_number + row_data.Description: description + row_data.Qty: quantity + row_data.Unit: unit + row_data.Part_Number: part_number + + # Steps 6 and 7 require custom executors (not built-in): + # - bom_excel_writer: writes .xlsx with red fill on rows where confidence < 0.90 + # - sharepoint_output: writes file back to SharePoint via Microsoft Graph API + + - executor: azure_blob_output + settings: + blob_container_name: bom-output + output_format: json +``` + +**Custom executor gaps required for full Phase 1**: +- `bom_excel_writer` — writes formatted Excel with `openpyxl`, applies `PatternFill(red)` to rows where confidence < 0.90 +- `sharepoint_output` — uploads file to SharePoint document library via Microsoft Graph API +- Extend `DocumentIntelligenceConnector.extract_tables()` to expose `cell.confidence` from the SDK response + +--- + +### 7.5 Compliance & PII Detection + +**Goal**: Scan documents for PII, classify by sensitivity level, and generate compliance risk reports. + +**Key executors**: `pdf_extractor` / `word_extractor` → `pii_detector` → `content_classifier` → `azure_openai_agent` → `azure_blob_output` + +```yaml +name: compliance-scan +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documents-review + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + settings: + extract_text: true + + - executor: pii_detector + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + pii_types: ["name", "email", "phone", "ssn", "credit_card", "address", "dob"] + redact: false + output_field: pii_results + + - executor: content_classifier + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + categories: ["public", "internal", "confidential", "restricted"] + include_confidence: true + output_field: classification + + - executor: azure_openai_agent + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + instructions: | + Based on PII findings and document classification, assess regulatory exposure: + - GDPR, HIPAA, PCI-DSS applicability + - Risk level: low / medium / high / critical + - Required remediation actions + output_field: compliance_report + + - executor: azure_blob_output + settings: + blob_container_name: compliance-reports +``` + +--- + +### 7.6 Web Content Ingestion + +**Goal**: Crawl product documentation or support sites, chunk content, generate embeddings, and index in Azure AI Search for a RAG chatbot. + +**Key executors**: `web_scraper` → `recursive_text_chunker` → `language_detector` → `azure_openai_embeddings` → `ai_search_index_output` + +```yaml +name: web-content-ingestion +steps: + - executor: web_scraper + settings: + urls: + - "https://docs.company.com" + - "https://support.company.com" + max_depth: 3 + selectors: ["article", "main", ".content"] + output_field: scraped_content + + - executor: recursive_text_chunker + settings: + chunk_size: 1500 + chunk_overlap: 200 + input_field: scraped_content.text + + - executor: language_detector + settings: + input_field: scraped_content.text + output_field: detected_language + + - executor: azure_openai_embeddings + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: text-embedding-3-small + + - executor: ai_search_index_output + settings: + endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" + index_name: web-content-rag +``` + +--- + +### 7.7 Multilingual Document Translation + +**Goal**: Detect language of incoming documents, translate to target languages, and index translated content. + +**Key executors**: `pdf_extractor` → `language_detector` → `recursive_text_chunker` → `content_translator` → `azure_openai_embeddings` → `ai_search_index_output` + +```yaml +name: multilingual-translation +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: original-content + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + settings: + extract_text: true + + - executor: language_detector + settings: + input_field: pdf_output.text + output_field: source_language + + - executor: recursive_text_chunker + settings: + chunk_size: 2000 + input_field: pdf_output.text + + - executor: content_translator + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + target_languages: ["en", "es", "fr", "de", "pt"] + output_field: translations + + - executor: azure_openai_embeddings + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: text-embedding-3-small + + - executor: ai_search_index_output + settings: + endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" + index_name: multilingual-content +``` + +--- + +### 7.8 Email Attachment Triage + +**Goal**: Process email attachments, detect sentiment, extract action items and deadlines, classify by category, and route to the appropriate team queue. + +**Key executors**: `pdf_extractor` → `sentiment_analyser` → `entity_extractor` → `azure_openai_agent` → `content_classifier` → `azure_blob_output` + +```yaml +name: email-triage +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: email-attachments + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + settings: + extract_text: true + + - executor: sentiment_analyser + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + granularity: document + include_confidence: true + output_field: sentiment + + - executor: entity_extractor + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + entity_types: ["person", "date", "organization", "action_item"] + output_field: entities + + - executor: azure_openai_agent + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + instructions: | + Extract from this email: + 1. Action items and who owns them + 2. Deadlines mentioned + 3. Urgency level: low / medium / high / critical + 4. Subject matter summary in one sentence + output_field: triage_result + + - executor: content_classifier + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + categories: ["technical_support", "billing", "complaint", "legal", "general_inquiry"] + output_field: category + + - executor: azure_blob_output + settings: + blob_container_name: email-triaged +``` + +--- + +### 7.9 Spreadsheet Data Pipeline + +**Goal**: Process Excel files row by row, normalize field names, validate data, and export cleaned records to Blob Storage as JSON. + +**Key executors**: `content_retriever` → `excel_extractor` → `table_row_splitter` → `field_mapper` → `field_selector` → `azure_blob_output` + +```yaml +name: spreadsheet-pipeline +steps: + - executor: content_retriever + settings: + use_temp_file_for_content: true + + - executor: excel_extractor + settings: + extract_text: false + extract_sheets: true + extract_tables: true + first_row_as_header: true + skip_hidden_sheets: true + output_field: excel_output + + - executor: table_row_splitter + settings: + table_field: excel_output.sheets.0.table + table_format: auto + has_header: true + skip_empty_rows: true + include_row_index: true + + - executor: field_mapper + settings: + mappings: + row_data.CustomerID: customer_id + row_data.CustomerName: name + row_data.EmailAddress: email + row_data.PurchaseAmount: amount + + - executor: field_selector + settings: + include_fields: ["customer_id", "name", "email", "amount", "row_index"] + + - executor: azure_blob_output + settings: + blob_container_name: processed-data + output_format: json +``` + +--- + +### 7.10 Knowledge Graph Construction + +**Goal**: Extract entities and relationships from a corporate document corpus and build a structured knowledge graph for organizational intelligence. + +**Key executors**: `azure_document_intelligence_extractor` → `recursive_text_chunker` → `entity_extractor` → `azure_openai_agent` → `document_set_initializer` → `cross_document_comparison` → `azure_blob_output` + +```yaml +name: knowledge-graph +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: corporate-documents + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + settings: + doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" + model_id: prebuilt-layout + extract_text: true + + - executor: recursive_text_chunker + settings: + chunk_size: 2000 + chunk_overlap: 100 + input_field: doc_intell_output.text + + - executor: entity_extractor + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + entity_types: ["person", "organization", "product", "location", "project", "technology"] + output_field: entities + + - executor: azure_openai_agent + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + instructions: | + From the extracted entities, identify relationships such as: + works_at, manages, reports_to, located_in, provides, uses, competes_with + Return as structured JSON with relationship triples. + output_field: relationships + + - executor: azure_blob_output + settings: + blob_container_name: knowledge-graphs + output_format: json +``` + +--- + +## 8. Included Sample Pipelines + +The repository includes 28+ ready-to-run sample pipelines in `contentflow-lib/samples/`: + +| Folder | Sample Name | What it demonstrates | +|---|---|---| +| `01-simple/` | Simple pipeline | Minimal pipeline: content retrieval + Document Intelligence | +| `02-batch-processing/` | Batch processing | Processing multiple documents concurrently | +| `03-pdf-extractor_chunker/` | PDF + Chunking | PyMuPDF extraction + recursive text chunker | +| `04-word-extractor/` | Word extractor | `.docx` content extraction with metadata | +| `05-powerpoint-extractor/` | PowerPoint extractor | `.pptx` slide-by-slide extraction | +| `06-ai-analysis/` | AI analysis | GPT-4 analysis with configurable prompts | +| `07-embeddings/` | Embeddings | Azure OpenAI embedding generation | +| `08-content-understanding/` | Content Understanding | Azure Content Understanding prebuilt models | +| `09-blob-input/` | Blob input | Full blob discovery + retrieval + Document Intelligence | +| `10-table-row-splitter/` | Table row splitter | Splitting tables into per-row content items | +| `11-excel-extractor/` | Excel extractor | Full Excel workbook extraction | +| `12-field-transformation/` | Field transformation | Field mapping, selection, and normalization | +| `13-blob-output-sample/` | Blob output | Writing processed results to Blob Storage | +| `14-gpt-rag-ingestion/` | GPT-RAG ingestion | Complete end-to-end RAG knowledge base pipeline | +| `15-document-analysis/` | Document analysis | Full Document Intelligence analysis workflow | +| `16-spreadsheet-pipeline/` | Spreadsheet pipeline | Excel ingestion → row splitting → field normalization | +| `17-knowledge-graph/` | Knowledge graph | Entity + relationship extraction for graph construction | +| `18-web-scraping/` | Web scraping | Playwright-based crawling and content extraction | +| `19-sub-pipelines/` | Sub-pipelines | Pipeline composition and nested workflow execution | +| `20-document-set-static/` | Document set (static) | Fixed multi-document cross-analysis | +| `21-document-set-comparison/` | Document comparison | Side-by-side AI comparison of two documents | +| `22-document-set-dynamic/` | Document set (dynamic) | Dynamically assembled document sets | +| `23-inline-document-set/` | Inline document set | Document sets defined inline in the pipeline | +| `27-subpipeline-processing/` | Subpipeline processing | Advanced sub-pipeline patterns | +| `28-advanced-batch/` | Advanced batch | Complex batch processing with error handling | +| `32-parallel-processing/` | Parallel processing | Fan-out/fan-in with concurrent branches | +| `44-conditional-routing/` | Conditional routing | If/else branching based on content properties | + +**Parallel workflow patterns** (documented in `PARALLEL_WORKFLOWS_GUIDE.md`): +- `fan_out_fan_in.yaml` — Split into parallel branches, merge results +- `split_merge.yaml` — Split content list, process in parallel, collect results +- `batch_subworkflow_example.yaml` — Batch processing with nested subworkflows +- `conditional_routing.yaml` — Content-aware routing based on classification results + +--- + +*Document version: March 25, 2026 — Based on ContentFlow repository branch `local-documentation`* diff --git a/Untitled.md b/Untitled.md new file mode 100644 index 0000000..e69de29 From a24019e615a581c31f1acaa0bc14a89c331e2b24 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 27 Mar 2026 12:18:52 -0400 Subject: [PATCH 21/29] remoteBuild fix --- Changes.md | 144 +++++++++++++++++++++++++++++++++ azure.yaml | 3 - infra/README.md | 4 +- infra/scripts/pre-provision.sh | 8 ++ 4 files changed, 154 insertions(+), 5 deletions(-) create mode 100644 Changes.md diff --git a/Changes.md b/Changes.md new file mode 100644 index 0000000..8b252b7 --- /dev/null +++ b/Changes.md @@ -0,0 +1,144 @@ +# Deployment Fixes – `azd deploy` in `ailz-integrated` mode + +## Table of Contents + +- [1. Fix: `remoteBuild: true` Incompatible with Private ACR](#1-fix-remotebuild-true-incompatible-with-private-acr) + +--- + +## 1. Fix: `remoteBuild: true` Incompatible with Private ACR + +**Error:** + +``` +failed to login, ran out of retries: failed to set docker credentials: +Error response from daemon: Get "https://crmpuiourm56df6.azurecr.io/v2/": denied: +client with IP '104.208.200.68' is not allowed access. +``` + +Followed by the local Docker fallback also failing: + +``` +Local fallback unavailable: the docker service is not running, please start it: +exit code: 1, stderr: permission denied while trying to connect to the Docker API +at unix:///var/run/docker.sock +``` + +**Root Cause:** + +`remoteBuild: true` in `azure.yaml` causes `azd` to use ACR Tasks to build container images. ACR Tasks run from public Azure infrastructure IPs. In `ailz-integrated` mode the ACR is provisioned with `publicNetworkAccess: Disabled`, so those IPs are rejected at the network level. This is a hard incompatibility [documented by Microsoft](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-private-link#use-az-acr-build-with-private-endpoint-and-private-registry): *"If you disable public network access, `az acr build` commands will fail."* + +When `azd` fell back to a local Docker build, a second issue blocked it: the JumpBox user `aiuser` was not a member of the `docker` group, causing `permission denied` on `/var/run/docker.sock`. The Docker daemon itself was running correctly. + +**Files:** `azure.yaml`, `infra/scripts/pre-provision.sh`, `infra/README.md` + +--- + +### `azure.yaml` — Remove `remoteBuild: true` from all services + +**Before:** + +```yaml +services: + api: + project: ./contentflow-api + language: py + host: containerapp + docker: + path: ./Dockerfile + context: .. + remoteBuild: true + worker: + project: ./contentflow-worker + language: py + host: containerapp + docker: + path: ./Dockerfile + context: .. + remoteBuild: true + web: + project: ./contentflow-web + language: ts + host: containerapp + docker: + path: ./Dockerfile + remoteBuild: true +``` + +**After:** + +```yaml +services: + api: + project: ./contentflow-api + language: py + host: containerapp + docker: + path: ./Dockerfile + context: .. + worker: + project: ./contentflow-worker + language: py + host: containerapp + docker: + path: ./Dockerfile + context: .. + web: + project: ./contentflow-web + language: ts + host: containerapp + docker: + path: ./Dockerfile +``` + +Removing `remoteBuild` makes `azd` always perform a local Docker build from the machine running the command. In `basic` mode the local machine pushes to the ACR public endpoint. In `ailz-integrated` mode the JumpBox is inside the AILZ VNet and can push via the ACR private endpoint. Both modes functionally equivalent; local build was already an implicit prerequisite since Docker is declared as a required tool. + +--- + +### `infra/scripts/pre-provision.sh` — Validate Docker daemon access + +**Before:** + +```bash +echo "✓ Checking Docker installation..." +if ! command -v docker &> /dev/null; then + echo "❌ Docker is not installed." + exit 1 +fi +``` + +**After:** + +```bash +echo "✓ Checking Docker installation..." +if ! command -v docker &> /dev/null; then + echo "❌ Docker is not installed." + exit 1 +fi + +echo "✓ Checking Docker daemon..." +if ! docker info &> /dev/null; then + echo "❌ Docker daemon is not running or current user cannot connect to it." + echo " For Linux: ensure dockerd is running and your user is in the 'docker' group." + echo " Run: sudo usermod -aG docker \$USER (then log out and back in)" + exit 1 +fi +``` + +The previous check only verified that the Docker CLI binary was installed, not that the daemon was reachable by the current user. Since a local Docker build is now required at deploy time, this catches the missing group membership issue early with a clear, actionable message. + +--- + +### `infra/README.md` — Update build step description + +**Before:** + +> Builds Docker images (remote build) +> Pushes to ACR +> Updates Container Apps + +**After:** + +> Builds Docker images locally (requires Docker daemon running on the machine executing `azd`) +> Pushes to ACR (via public endpoint in `basic` mode, via private endpoint in `ailz-integrated` mode — must be executed from within the AI LZ VNet, e.g. from the JumpBox VM) +> Updates Container Apps diff --git a/azure.yaml b/azure.yaml index f782d62..6d3853a 100644 --- a/azure.yaml +++ b/azure.yaml @@ -22,7 +22,6 @@ services: path: ./Dockerfile context: .. target: "" - remoteBuild: true hooks: postdeploy: shell: sh @@ -38,7 +37,6 @@ services: path: ./Dockerfile context: .. target: "" - remoteBuild: true # ContentFlow Web - Container App web: @@ -49,7 +47,6 @@ services: path: ./Dockerfile context: .. target: "" - remoteBuild: true # Global hooks hooks: diff --git a/infra/README.md b/infra/README.md index d68178f..35fb672 100644 --- a/infra/README.md +++ b/infra/README.md @@ -512,8 +512,8 @@ All scripts are bash scripts designed to run as azd hooks. - Validates infrastructure exists 5. **Service Deployment** (azd deploy) - - Builds Docker images (remote build) - - Pushes to ACR + - Builds Docker images locally (requires Docker daemon running on the machine executing `azd`) + - Pushes to ACR (via public endpoint in `basic` mode, via private endpoint in `ailz-integrated` mode — must be executed from within the AI LZ VNet, e.g. from the JumpBox VM) - Updates Container Apps 6. **post-deploy-{service}.sh** - After each service deployment diff --git a/infra/scripts/pre-provision.sh b/infra/scripts/pre-provision.sh index b1309ba..7221579 100755 --- a/infra/scripts/pre-provision.sh +++ b/infra/scripts/pre-provision.sh @@ -24,6 +24,14 @@ if ! command -v docker &> /dev/null; then exit 1 fi +echo "✓ Checking Docker daemon..." +if ! docker info &> /dev/null; then + echo "❌ Docker daemon is not running or current user cannot connect to it." + echo " For Linux: ensure dockerd is running and your user is in the 'docker' group." + echo " Run: sudo usermod -aG docker \$USER (then log out and back in)" + exit 1 +fi + echo "✓ Verifying Azure CLI login..." if ! az account show &> /dev/null; then echo "❌ Not logged in to Azure CLI. Please run 'az login' first." From 6803e2c1ef017102d872abcd81241ffeccce5b70 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 27 Mar 2026 12:27:29 -0400 Subject: [PATCH 22/29] remoteBuild fix --- .gitignore | 5 +- Analysis/00-full-analysis.md | 1129 --------------- Analysis/01-overview.md | 430 ------ Analysis/02-architecture-detailed.md | 1920 -------------------------- Analysis/03-use_cases_examples.md | 666 --------- Analysis/04-ai_lz_options.md | 1224 ---------------- Analysis/05-mini-ailz.md | 1002 -------------- personal-private-configurations.md | 141 -- 8 files changed, 4 insertions(+), 6513 deletions(-) delete mode 100644 Analysis/00-full-analysis.md delete mode 100644 Analysis/01-overview.md delete mode 100644 Analysis/02-architecture-detailed.md delete mode 100644 Analysis/03-use_cases_examples.md delete mode 100644 Analysis/04-ai_lz_options.md delete mode 100644 Analysis/05-mini-ailz.md delete mode 100644 personal-private-configurations.md diff --git a/.gitignore b/.gitignore index 6e1d706..1f51dd7 100644 --- a/.gitignore +++ b/.gitignore @@ -206,4 +206,7 @@ marimo/_lsp/ __marimo__/ -gitignore* \ No newline at end of file +gitignore* + +# Local analysis files +.Analysis/ diff --git a/Analysis/00-full-analysis.md b/Analysis/00-full-analysis.md deleted file mode 100644 index 37e81e8..0000000 --- a/Analysis/00-full-analysis.md +++ /dev/null @@ -1,1129 +0,0 @@ -# ContentFlow — Complete Reference Analysis - -> **Single-file reference document for AI agents and developers.** -> This document consolidates all capabilities, architecture, prerequisites, out-of-scope boundaries, and example scenarios for the ContentFlow accelerator. Use it as the authoritative context source before working with this codebase. - ---- - -## Table of Contents - -1. [What is ContentFlow?](#1-what-is-contentflow) -2. [Core Concepts](#2-core-concepts) -3. [Full Capabilities](#3-full-capabilities) - - [3.1 Built-in Executors (37 total)](#31-built-in-executors-37-total) - - [3.2 Pipeline Engine Features](#32-pipeline-engine-features) - - [3.3 Web UI Features](#33-web-ui-features) - - [3.4 API Features](#34-api-features) - - [3.5 Worker Engine Features](#35-worker-engine-features) - - [3.6 Extensibility: Custom Executors](#36-extensibility-custom-executors) -4. [Prerequisites](#4-prerequisites) - - [4.1 Azure Services Required](#41-azure-services-required) - - [4.2 Developer Toolchain](#42-developer-toolchain) - - [4.3 Azure Permissions](#43-azure-permissions) - - [4.4 Service Quotas & Limits](#44-service-quotas--limits) -5. [Final Architecture](#5-final-architecture) - - [5.1 Component Overview](#51-component-overview) - - [5.2 Service Communication Map](#52-service-communication-map) - - [5.3 Data Flow](#53-data-flow) - - [5.4 Task Lifecycle](#54-task-lifecycle) - - [5.5 Cosmos DB Schema](#55-cosmos-db-schema) - - [5.6 Deployment Modes](#56-deployment-modes) - - [5.7 Security Model](#57-security-model) -6. [Out of Scope](#6-out-of-scope) -7. [Example Scenarios](#7-example-scenarios) - - [7.1 Invoice & Receipt Processing](#71-invoice--receipt-processing) - - [7.2 Enterprise Knowledge Base for RAG](#72-enterprise-knowledge-base-for-rag) - - [7.3 Contract Analysis](#73-contract-analysis) - - [7.4 Engineering PDF BOM Extraction](#74-engineering-pdf-bom-extraction) - - [7.5 Compliance & PII Detection](#75-compliance--pii-detection) - - [7.6 Web Content Ingestion](#76-web-content-ingestion) - - [7.7 Multilingual Document Translation](#77-multilingual-document-translation) - - [7.8 Email Attachment Triage](#78-email-attachment-triage) - - [7.9 Spreadsheet Data Pipeline](#79-spreadsheet-data-pipeline) - - [7.10 Knowledge Graph Construction](#710-knowledge-graph-construction) -8. [Included Sample Pipelines](#8-included-sample-pipelines) - ---- - -## 1. What is ContentFlow? - -**ContentFlow** is an open-source, enterprise-grade, cloud-native document and content processing accelerator built on Azure. It transforms unstructured content — PDFs, Word documents, Excel files, PowerPoint presentations, CSV files, web pages, images — into structured, intelligent, actionable data through orchestrated AI-powered pipelines. - -**Primary value proposition**: ContentFlow eliminates the infrastructure plumbing required to build scalable document processing systems on Azure. A developer can go from zero to a production-grade pipeline in hours rather than weeks by combining pre-built executors declaratively via YAML. - -**Repository**: [Azure/contentflow](https://github.com/Azure/contentflow) -**License**: MIT -**Runtime**: Python 3.12+ (backend), TypeScript/React 18 (frontend) - ---- - -## 2. Core Concepts - -### Pipeline -A **pipeline** is the fundamental unit of work. It defines an ordered sequence of executors that content passes through. Pipelines are declared in YAML and stored in Cosmos DB. - -```yaml -name: my-pipeline -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documents - - executor: azure_document_intelligence_extractor - settings: - doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" - - executor: recursive_text_chunker - settings: - chunk_size: 1000 - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - executor: ai_search_index_output - settings: - index_name: my-index -``` - -### Executor -An **executor** is a self-contained processing unit. Each executor receives a `Content` object, transforms or enriches it, and passes the result to the next step. Executors are composable, configurable via YAML settings, and can run in parallel. - -### Vault -A **vault** is a logical container that associates a document source with a pipeline. When enabled, the Worker service automatically discovers new documents arriving in the vault and queues them for processing. - -### Content Model -The standardized Python object (`Content`) that flows through every pipeline step: - -```python -Content - ├── id: ContentIdentifier # canonical_id, source_type, container, path, filename - ├── data: dict # All executor outputs accumulate here - ├── summary_data: dict # Execution metrics per executor - └── executor_logs: list # Per-step audit trail -``` - -All executor outputs are stored as named fields in `content.data`, enabling downstream executors to reference previous results via dot-notation paths (e.g., `doc_intell_output.tables`). - ---- - -## 3. Full Capabilities - -### 3.1 Built-in Executors (37 total) - -#### Input Executors (3) -| Executor ID | Description | -|---|---| -| `azure_blob_input_discovery` | Discovers files in Azure Blob Storage containers. Supports file extension filtering, prefix filtering, and incremental crawling via checkpoints (processes only new/modified blobs since last run). | -| `azure_blob_content_retriever` | Downloads blob content as bytes or saves to a temp file path for downstream processing. Supports parallel retrieval with configurable concurrency. | -| `content_retriever` | Loads content from local file system or arbitrary byte sources. Primarily used in local development and sample pipelines. | - -#### Document Extraction Executors (6) -| Executor ID | Description | -|---|---| -| `azure_document_intelligence_extractor` | Invokes Azure Document Intelligence (prebuilt-layout, prebuilt-document, prebuilt-read) to extract text, tables, key-value pairs, and figures from any document. Supports markdown or plain text output format, page range selection, locale hints, and additional features like `ocrHighResolution`. | -| `azure_content_understanding_extractor` | Uses Azure Content Understanding for advanced document analysis including prebuilt invoice, receipt, business card, and ID models. | -| `pdf_extractor` | Extracts text, page-level chunks, and embedded images from PDF files using PyMuPDF. No Azure service dependency — runs locally. | -| `word_extractor` | Extracts text, headings, tables, and metadata from `.docx` files using python-docx. | -| `powerpoint_extractor` | Extracts slide content, speaker notes, embedded images, and metadata from `.pptx` files using python-pptx. | -| `excel_extractor` | Extracts sheets, tables, cell values, formulas, images, and metadata from `.xlsx`/`.xlsm` files using openpyxl. Supports first-row-as-header detection, sheet filtering, and table range detection. | -| `csv_extractor` | Parses CSV files with configurable delimiter, quoting, and encoding. | - -#### AI Processing Executors (8) -| Executor ID | Description | -|---|---| -| `azure_openai_agent` | Sends content to Azure OpenAI (GPT-4.1, GPT-4.1-mini, etc.) with a configurable system prompt and user template. The most flexible executor — suitable for summarization, extraction, classification, transformation, and any custom reasoning task. | -| `azure_openai_embeddings` | Generates vector embeddings using Azure OpenAI embedding models (text-embedding-3-small, text-embedding-3-large). Output format compatible with Azure AI Search vector fields. | -| `text_summarizer` | Generates concise summaries with configurable max length and style. Built on `azure_openai_agent`. | -| `entity_extractor` | Extracts named entities (persons, organizations, dates, amounts, locations) from text. Configurable entity types. Built on `azure_openai_agent`. | -| `sentiment_analyser` | Document-level, sentence-level, or aspect-based sentiment analysis with confidence scores. Supports 3-point and 5-point scales and emotion detection. | -| `content_classifier` | Multi-class and multi-label classification with confidence scores and explanations. Accepts custom category lists and descriptions. | -| `pii_detector` | Detects and optionally redacts Personally Identifiable Information (PII) such as names, emails, phone numbers, SSNs, credit cards, addresses. | -| `keyword_extractor` | Extracts top-N keywords and key phrases from text content. | - -#### Transformation Executors (6) -| Executor ID | Description | -|---|---| -| `recursive_text_chunker` | Splits long text into overlapping chunks using recursive character splitting with configurable chunk size, overlap, and separators. | -| `table_row_splitter` | Splits tabular data (list-of-lists, list-of-dicts, CSV string, Word tables, Excel rows) into individual `Content` objects — one per row — enabling per-row parallel processing downstream. | -| `field_mapper` | Renames, remaps, and transforms fields within `content.data` using a declarative mapping configuration. | -| `field_selector` | Includes or excludes specific fields from `content.data` to control what data is passed forward. | -| `language_detector` | Detects the primary language of text content. | -| `content_translator` | Translates content to one or more target languages using Azure OpenAI. | - -#### Output Executors (3) -| Executor ID | Description | -|---|---| -| `azure_blob_output` | Writes processed content (JSON, text, binary) back to Azure Blob Storage. Supports custom path templates and content serialization. | -| `ai_search_index_output` | Indexes processed content into Azure AI Search. Supports vector fields, semantic configuration, and batch indexing. | -| `gptrag_search_index_document_generator` | Generates documents in GPT-RAG-compatible format for Azure AI Search, including chunked content and embeddings for RAG scenarios. | - -#### Control Flow Executors (5) -| Executor ID | Description | -|---|---| -| `subpipeline` | Executes another pipeline as a nested sub-workflow. Enables pipeline composition and reuse. | -| `for_each_content` | Iterates over a list of content items and applies a sub-pipeline to each one. | -| `fan_in_aggregator` | Collects and merges results from parallel branches back into a single content stream. | -| `document_set_initializer` | Initializes a multi-document analysis session for cross-document operations. | -| `document_set_collector` | Collects documents into a set for batch cross-document processing. | - -#### Cross-Document Executors (3) -| Executor ID | Description | -|---|---| -| `cross_document_comparison` | Compares multiple documents side-by-side using AI to identify similarities, differences, and patterns. | -| `cross_document_field_aggregator` | Aggregates specific fields across a document set (e.g., collect all "summary" fields from 50 documents). | -| `cosmos_db_lookup` | Looks up metadata or previously stored results from Cosmos DB during pipeline execution. Useful for enrichment and deduplication. | - -#### Utility Executors (3) -| Executor ID | Description | -|---|---| -| `pass_through` | No-op executor. Useful for pipeline testing and debugging. | -| `web_scraper` | Crawls web pages using Playwright (headless Chromium). Supports CSS selectors, depth control, and JavaScript-rendered pages. | -| `cosmos_db_lookup` | Retrieves enrichment data from Cosmos DB based on a key field in the content. | - ---- - -### 3.2 Pipeline Engine Features - -- **Declarative YAML definition** — pipelines are stored as YAML and parsed at runtime -- **DAG execution** — pipelines can define explicit `execution_sequence` for dependency control -- **Async/await throughout** — all executors are async; built on Python asyncio -- **Parallel execution** — `ParallelExecutor` base class enables `max_concurrent` processing (default: 3 concurrent items) -- **Conditional execution** — any executor supports a `condition` setting (Python expression evaluated against `content.data`) -- **Error handling** — `fail_pipeline_on_error` and `continue_on_error` per executor; up to 3 retries with configurable delay -- **Timeout control** — per-executor `timeout_secs` setting -- **Debug mode** — verbose logging per executor via `debug_mode: true` -- **Nested fields** — all settings support dot-notation for accessing nested data (e.g., `doc_intell_output.tables`) -- **Environment variable substitution** — any setting value can reference env vars with `${VAR_NAME}` syntax -- **Sub-pipelines** — pipelines can invoke other pipelines as steps, enabling workflow composition -- **Parallel fan-out/fan-in** — native support for splitting content into parallel branches and merging results -- **Incremental processing** — vault crawl checkpoints prevent duplicate reprocessing - ---- - -### 3.3 Web UI Features - -**Technology**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn/ui, Radix UI, ReactFlow, Monaco Editor - -| Feature | Description | -|---|---| -| **Visual Pipeline Builder** | Drag-and-drop graphical interface built on ReactFlow. Nodes represent executors; edges represent data flow. | -| **YAML Editor** | Monaco-powered code editor with syntax highlighting for editing pipeline definitions directly. | -| **Template Gallery** | Pre-built pipeline templates for common scenarios, ready to clone and customize. | -| **Vault Management** | Create, edit, enable/disable document vaults. Associate vaults with pipelines. | -| **Execution Dashboard** | Real-time view of pipeline execution status, metrics, and per-executor output. | -| **Executor Catalog Browser** | Browse all 37 executors with descriptions, settings schemas, and usage examples. | -| **Knowledge Graph Viewer** | Visualization of entity-relationship graphs produced by knowledge graph pipelines. | - -**Routes**: -- `/` — Home dashboard -- `/?view=pipeline` — Pipeline builder -- `/?view=vaults` — Vault management -- `/?view=graph` — Knowledge graph visualization -- `/templates` — Template gallery - ---- - -### 3.4 API Features - -**Technology**: FastAPI 0.128, Python 3.12+, Uvicorn, Pydantic v2, Azure SDK -**Port**: 8090 -**Authentication**: Azure Managed Identity (no API keys in production) - -| Endpoint group | Operations | -|---|---| -| `GET /api/health` | System health check — verifies Cosmos DB, Blob, Queue, App Config connectivity | -| `GET/POST/PUT/DELETE /api/pipelines` | Full CRUD for pipeline definitions; trigger manual execution | -| `GET/POST/PUT/DELETE /api/vaults` | Vault management including enable/disable | -| `GET /api/vaults/{id}/executions` | Vault execution history and status | -| `GET /api/executors` | Browse executor catalog with full settings schemas | -| `GET /docs` | Interactive Swagger UI | -| `GET /redoc` | ReDoc documentation | - ---- - -### 3.5 Worker Engine Features - -**Technology**: Python 3.12+, multiprocessing, Azure Storage Queue -**Port**: 8099 (health API) - -| Feature | Description | -|---|---| -| **Multi-process architecture** | N Source Workers + M Processing Workers run as independent OS processes | -| **Source Workers** | Poll enabled vaults every 5 minutes (configurable), discover new content, enqueue tasks | -| **Processing Workers** | Poll Azure Storage Queue, execute full pipelines on content items | -| **Incremental crawling** | Checkpoints stored in Cosmos DB prevent reprocessing already-seen documents | -| **Distributed locking** | Lock TTL prevents duplicate processing when multiple workers poll simultaneously | -| **Auto-restart** | Monitor loop detects crashed worker processes and replaces them automatically | -| **Graceful shutdown** | SIGINT/SIGTERM handled via shared `multiprocessing.Event`; workers finish current task before stopping | -| **Configurable parallelism** | `NUM_PROCESSING_WORKERS` and `NUM_SOURCE_WORKERS` env vars | -| **Task retries** | Up to 3 retries per task with configurable delay | - ---- - -### 3.6 Extensibility: Custom Executors - -ContentFlow is designed for extension. New executors can be created by subclassing one of three base classes: - -| Base Class | Use case | -|---|---| -| `BaseExecutor` | Single content item or list processing; implement `process_input()` | -| `ParallelExecutor` | Automatic parallel processing; implement `process_content_item()` once; framework handles concurrency | -| `InputExecutor` | Input source discovery; implement `crawl()` to yield new content items | - -After implementing the class: -1. Register it in `executor_catalog.yaml` with its `id`, `module_path`, `class_name`, tags, and `settings_schema` -2. Import it in `contentflow/executors/__init__.py` - -Custom executors work identically to built-in ones — they appear in the Web UI, accept YAML configuration, support all base class features (debug mode, conditions, retries, timeouts), and integrate with the full pipeline engine. - ---- - -## 4. Prerequisites - -### 4.1 Azure Services Required - -| Service | SKU (Basic mode) | SKU (AILZ mode) | Purpose | -|---|---|---|---| -| **Azure Container Apps Environment** | Consumption | Consumption (Internal) | Hosts API, Worker, Web containers | -| **Azure Cosmos DB** | Serverless | Serverless + Private Endpoint | Pipeline definitions, execution state, vault metadata | -| **Azure Blob Storage** | Standard LRS, Hot | Standard LRS + Private Endpoint | Document storage, processed outputs | -| **Azure Storage Queue** | Standard | Standard + Private Endpoint | Task queue between Source and Processing Workers | -| **Azure App Configuration** | Standard | Standard + Private Endpoint | Centralized runtime configuration | -| **Azure Application Insights** | Pay-per-use | Pay-per-use | Distributed tracing, monitoring | -| **Azure Log Analytics** | PerGB2018 | PerGB2018 (shared) | Log aggregation | -| **Azure Container Registry** | Standard | **Premium** (required for Private Endpoint) | Container image registry | -| **Azure Managed Identity** | User-assigned | User-assigned | Passwordless authentication between all services | - -**Optional Azure services** (required for specific executors): - -| Service | Required by executor(s) | -|---|---| -| **Azure AI Foundry / OpenAI** | `azure_openai_agent`, `azure_openai_embeddings`, `text_summarizer`, `entity_extractor`, `sentiment_analyser`, `content_classifier`, `pii_detector`, `keyword_extractor`, `content_translator` | -| **Azure Document Intelligence** | `azure_document_intelligence_extractor` | -| **Azure Content Understanding** | `azure_content_understanding_extractor` | -| **Azure AI Search** | `ai_search_index_output`, `gptrag_search_index_document_generator` | - -### 4.2 Developer Toolchain - -```bash -# Required for local development -python --version # 3.12+ -node --version # 18+ -npm --version # 9+ -docker --version # Any recent version -az --version # Azure CLI 2.60+ -azd version # Azure Developer CLI 1.5+ - -# Required for deployment -git --version # Any recent version -``` - -**Python dependencies** (installed from `requirements.txt` per service): -- `fastapi>=0.128.0`, `uvicorn>=0.40.0`, `pydantic>=2.12` -- `azure-identity>=1.25.1`, `azure-cosmos>=4.14.3` -- `azure-storage-blob>=12.27.1`, `azure-storage-queue>=12.14.1` -- `azure-appconfiguration-provider>=2.3.1` -- `azure-ai-documentintelligence` (for Document Intelligence) -- `openai` (for Azure OpenAI) -- `pymupdf` (for PDF extraction) -- `python-docx`, `python-pptx`, `openpyxl` (Office documents) -- `playwright` (for web scraping — requires browser install) -- `agent-framework` (Microsoft Agent Framework — pipeline orchestration) - -### 4.3 Azure Permissions - -| Permission | Scope | Required for | -|---|---|---| -| `Contributor` | ContentFlow Resource Group | Deploy all resources | -| `User Access Administrator` | ContentFlow Resource Group | Assign RBAC to Managed Identity | -| `Network Contributor` | VNet Resource Group | Create Private Endpoints (AILZ mode only) | -| `Private DNS Zone Contributor` | DNS Zone Resource Group | Register PE records (AILZ mode only) | - -**RBAC roles assigned automatically to the Managed Identity during deployment**: -- `Storage Blob Data Contributor` on Storage Account -- `Storage Queue Data Contributor` on Storage Account -- `Cosmos DB Built-in Data Contributor` on Cosmos DB -- `App Configuration Data Reader` on App Configuration -- `AcrPull` on Container Registry -- `Cognitive Services User` on AI Services (when applicable) - -### 4.4 Service Quotas & Limits - -| Limit | Default | Notes | -|---|---|---| -| Container Apps replicas | 1–2 per service | Auto-scales based on load | -| Worker processes | Configurable | `NUM_PROCESSING_WORKERS` env var | -| Queue message visibility timeout | 300 seconds | Task must complete within 5 minutes or is retried | -| Task max retries | 3 | Configurable via pipeline settings | -| Task timeout | 600 seconds | 10 minutes per pipeline execution | -| Cosmos DB throughput | Serverless (on-demand) | No pre-provisioned RU/s needed | -| Azure OpenAI TPM | Varies by deployment | Should be sized for expected concurrency | - ---- - -## 5. Final Architecture - -### 5.1 Component Overview - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ AZURE CONTAINER APPS ENVIRONMENT │ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ -│ │ contentflow- │ │ contentflow- │ │ contentflow- │ │ -│ │ web │ │ api │ │ worker │ │ -│ │ │ │ │ │ │ │ -│ │ React 18 + │──▶│ FastAPI │ │ Multi-process │ │ -│ │ TypeScript │ │ Python 3.12+ │ │ Python 3.12+ │ │ -│ │ Port: 80 │ │ Port: 8090 │ │ Port: 8099 │ │ -│ │ │ │ │ │ (health only) │ │ -│ │ Pipeline │ │ Routers: │ │ │ │ -│ │ Builder UI │ │ /pipelines │ │ Source Workers │ │ -│ │ Vault Manager │ │ /vaults │ │ (crawl vaults) │ │ -│ │ Exec Dashboard │ │ /executors │ │ │ │ -│ │ Template Gall. │ │ /health │ │ Processing │ │ -│ └─────────────────┘ └────────┬────────┘ │ Workers │ │ -│ │ │ (run pipelines) │ │ -└──────────────────────────────────┼────────────┴──────────────────┘ │ - │ │ - ┌─────────────────────────┼────────────────────┼──────────────┐ - │ ▼ ▼ │ - │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ - │ │ Cosmos DB │ │ Blob Storage │ │ Storage Queue │ │ - │ │ (Serverless)│ │ (LRS, Hot) │ │ (processing │ │ - │ │ │ │ │ │ tasks) │ │ - │ │ pipelines │ │ vaults/ │ │ │ │ - │ │ vaults │ │ processed/ │ │ │ │ - │ │ executions │ │ outputs/ │ │ │ │ - │ │ checkpoints │ │ │ │ │ │ - │ └──────────────┘ └──────────────┘ └─────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ - │ │ App Config │ │ App Insights│ │ Container │ │ - │ │ (Standard) │ │ + Log Anal. │ │ Registry │ │ - │ └──────────────┘ └──────────────┘ └─────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ - │ │ AI Foundry │ │ Document │ │ Azure AI │ │ - │ │ (OpenAI) │ │ Intelligence│ │ Search │ │ - │ │ (optional) │ │ (optional) │ │ (optional) │ │ - │ └──────────────┘ └──────────────┘ └─────────────────┘ │ - │ AZURE MANAGED SERVICES │ - └──────────────────────────────────────────────────────────────┘ -``` - -### 5.2 Service Communication Map - -| From | To | Protocol | Auth | -|---|---|---|---| -| Web UI | ContentFlow API | HTTPS REST | None (same environment) | -| API | Cosmos DB | Azure SDK over HTTPS | Managed Identity | -| API | Blob Storage | Azure SDK over HTTPS | Managed Identity | -| API | Azure App Configuration | Azure SDK over HTTPS | Managed Identity | -| Worker (Source) | Cosmos DB | Azure SDK over HTTPS | Managed Identity | -| Worker (Source) | Storage Queue | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Storage Queue | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Cosmos DB | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Blob Storage | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Azure OpenAI | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Document Intelligence | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Azure AI Search | Azure SDK over HTTPS | Managed Identity | - -### 5.3 Data Flow - -``` -1. CONTENT DISCOVERY - Source Worker - │ - ├── Query Cosmos DB → enabled vaults - ├── For each vault → read associated pipeline - ├── Run input executor (e.g., azure_blob_input_discovery) - ├── Discover new files since last checkpoint - ├── Create ContentProcessingTask per file - ├── Enqueue tasks → Azure Storage Queue - └── Save new crawl checkpoint → Cosmos DB - -2. CONTENT PROCESSING - Processing Worker - │ - ├── Poll Storage Queue (up to 32 messages) - ├── Deserialize ContentProcessingTask - ├── Acquire distributed lock (prevent duplicate processing) - ├── Load pipeline YAML from Cosmos DB - ├── For each pipeline step (skipping input executors): - │ ├── Instantiate executor - │ ├── Check condition (if set) - │ ├── Call executor.process_input(content) - │ └── Accumulate results in content.data - ├── Update execution status → Cosmos DB (COMPLETED / FAILED) - ├── Write output → Blob Storage (if configured) - └── Delete message from queue - -3. OUTPUT - Configured output executors write to: - ├── Azure Blob Storage (JSON, text, binary) - ├── Azure AI Search (indexed documents) - └── Any custom destination via custom executor -``` - -### 5.4 Task Lifecycle - -``` -[PENDING] ← Task created and enqueued - │ - ▼ -[RUNNING] ← Processing Worker picks up the task - │ - ├──── Success ──→ [COMPLETED] - │ │ - │ Store results in Cosmos DB + Blob - │ - ├──── Failure ──→ [FAILED] - │ │ - │ ├── retry_count < max_retries (3)? - │ │ └── Yes → re-enqueue → [PENDING] - │ └── No → [FAILED] (permanent) - │ - └──── Cancelled ─→ [CANCELLED] -``` - -### 5.5 Cosmos DB Schema - -**Database**: `contentflow-db` - -| Container | Partition Key | Contents | -|---|---|---| -| `pipelines` | `/id` | Pipeline YAML definitions, graph nodes/edges, metadata | -| `vaults` | `/id` | Vault configurations, associated pipeline IDs | -| `vault_executions` | `/vault_id` | Per-document execution records with status and outputs | -| `pipeline_executions` | `/pipeline_id` | Direct pipeline execution records | -| `vault_crawl_checkpoints` | `/vault_id` | Last crawl timestamp per vault+executor | -| `executor_locks` | `/task_id` | Distributed locks for deduplication | -| `executor_catalog` | `/id` | Cached executor catalog (loaded from YAML at startup) | - -### 5.6 Deployment Modes - -#### Basic Mode (Public Endpoints) -All Azure resources expose public endpoints. Suitable for development, demos, and non-regulated environments. - -``` -Internet → Container Apps (external ingress) → Azure Services (public endpoints) -``` - -**Deploy**: `azd up` (one command) - -#### AILZ-Integrated Mode (Private Endpoints) -All Azure resources have `publicNetworkAccess: Disabled`. Traffic flows only through Private Endpoints inside a VNet. Suitable for enterprise, regulated environments, and AI Landing Zone integration. - -``` -JumpBox/VPN → VNet → Container Apps (internal LB) → Azure Services (Private Endpoints) -``` - -**Differences from Basic**: -- Container Apps Environment: `internal: true` (no public ingress) -- All services: `publicNetworkAccess: Disabled` -- Network ACLs: `defaultAction: Deny`, `bypass: AzureServices` -- Container Registry: must be **Premium** SKU (required for Private Endpoint support) -- Requires pre-existing VNet with subnets: `pe-subnet` and `aca-env-subnet` -- Requires 6 Private DNS Zones linked to the VNet: - - `privatelink.blob.core.windows.net` - - `privatelink.queue.core.windows.net` - - `privatelink.documents.azure.com` - - `privatelink.azconfig.io` - - `privatelink.azurecr.io` - - `privatelink.cognitiveservices.azure.com` - -**Deploy**: `azd up` with `ailzMode=ailz-integrated` parameter and pre-existing network resource IDs. - -### 5.7 Security Model - -| Principle | Implementation | -|---|---| -| **Zero-Trust** | No passwords, no connection strings. All service-to-service communication uses Managed Identity. | -| **Passwordless** | `ChainedTokenCredential` (Managed Identity → `DefaultAzureCredential`) used across all Azure SDK calls. | -| **Least Privilege** | Each service's Managed Identity is assigned only the RBAC roles it needs on each resource. | -| **Network Isolation** | AILZ mode enforces private networking; Basic mode exposes public endpoints with HTTPS. | -| **Secrets Management** | All secrets are either environment variables injected at deployment time or resolved from Azure App Configuration. No secrets in code or Git. | -| **Centralized Config** | Azure App Configuration is the single source of truth for runtime settings; local `.env` is for development only. | -| **CORS** | API configures `allow_origins: ["*"]` (suitable for internal deployments; restrict in production). | - ---- - -## 6. Out of Scope - -The following capabilities are **not included** in ContentFlow out-of-the-box and would require custom executor development, additional Azure services, or integration work: - -| Capability | Notes | -|---|---| -| **SharePoint read/write connector** | `ContentIdentifier.source_type` mentions "sharepoint" but no executor or connector exists. Would require Microsoft Graph API integration as a custom executor. | -| **OneDrive connector** | Same as SharePoint — no native executor. The Web UI shows it as a planned input type. | -| **Excel/XLSX writer** | `ExcelExtractorExecutor` reads Excel but there is no executor to **write** formatted `.xlsx` files (e.g., with conditional formatting). Requires custom executor using openpyxl. | -| **Confidence score per BOM cell (Document Intelligence)** | The `extract_tables()` method in `DocumentIntelligenceConnector` returns cell content and position but does not expose `cell.confidence` from the SDK response. Requires a code extension to the connector. | -| **Email (Exchange/Outlook) connector** | No native email source executor. Would require Microsoft Graph API integration. | -| **Relational database connector** (SQL, PostgreSQL) | No native DB input executor. Would require a custom `InputExecutor` subclass. | -| **Real-time / streaming processing** | ContentFlow is a batch/async pipeline system. It does not support real-time streaming ingestion (e.g., Kafka, Event Hub consumer). | -| **Chat interface** | No conversational UI or chat bot. ContentFlow processes documents, it does not serve a chat endpoint. Integration with Copilot Studio or Azure Bot Service is out of scope. | -| **Azure AI Search index schema management** | The `ai_search_index_output` executor writes to an existing index but does not create or manage index schemas. Index schema provisioning must happen separately. | -| **Custom Document Intelligence model training** | ContentFlow uses pre-built or previously trained models. Training custom Document Intelligence models is outside the accelerator's scope. | -| **Dynamics 365 / SAP integration** | No native connectors for line-of-business systems. Outputs to Blob or AI Search can be consumed by downstream integrations. | -| **Multi-tenant isolation** | All pipelines and vaults within a deployment share the same Cosmos DB and Blob Storage. There is no tenant-level data separation built in. | -| **Role-based access control in the UI** | The Web UI has no authentication or authorization layer. It is intended for internal use by platform operators. | -| **Built-in pipeline scheduling** | Vaults trigger continuous background polling; there is no cron-style scheduler. Time-based triggers must be implemented externally (e.g., Logic Apps, Azure Functions). | -| **Video or audio processing** | No native executors for audio transcription or video analysis. These would require Azure Speech or Azure Video Indexer custom executors. | - ---- - -## 7. Example Scenarios - -### 7.1 Invoice & Receipt Processing - -**Goal**: Extract structured data from incoming invoices PDFs and images; classify by document type; flag anomalies; output to Blob Storage. - -**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_content_understanding_extractor` (prebuilt-invoice) → `content_classifier` → `field_mapper` → `azure_blob_output` - -```yaml -name: invoice-processing -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: invoices-incoming - file_extensions: [".pdf", ".png", ".jpg"] - - - executor: azure_blob_content_retriever - - - executor: azure_content_understanding_extractor - settings: - model_id: prebuilt-invoice - output_field: invoice_data - - - executor: content_classifier - settings: - categories: ["invoice", "receipt", "purchase_order", "credit_note"] - include_confidence: true - - - executor: field_mapper - settings: - mappings: - invoice_data.VendorName: vendor_name - invoice_data.InvoiceTotal.amount: total_amount - invoice_data.InvoiceDate: invoice_date - - - executor: azure_blob_output - settings: - blob_container_name: invoices-processed - output_format: json -``` - ---- - -### 7.2 Enterprise Knowledge Base for RAG - -**Goal**: Process a mixed-format document library (PDF, Word, Excel), chunk text, generate embeddings, and index in Azure AI Search to power a RAG chatbot. - -**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_document_intelligence_extractor` → `recursive_text_chunker` → `azure_openai_embeddings` → `gptrag_search_index_document_generator` → `ai_search_index_output` - -```yaml -name: knowledge-base-ingestion -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: company-documents - file_extensions: [".pdf", ".docx", ".xlsx"] - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - settings: - doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" - model_id: prebuilt-layout - extract_text: true - extract_tables: true - - - executor: recursive_text_chunker - settings: - chunk_size: 800 - chunk_overlap: 150 - input_field: doc_intell_output.text - - - executor: azure_openai_embeddings - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: text-embedding-3-small - input_field: chunks - - - executor: gptrag_search_index_document_generator - settings: - index_name: company-knowledge-base - - - executor: ai_search_index_output - settings: - endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" - index_name: company-knowledge-base -``` - ---- - -### 7.3 Contract Analysis - -**Goal**: Extract obligations, parties, dates, monetary values, and risk clauses from legal contracts. Generate an executive summary per document. - -**Key executors**: `pdf_extractor` → `entity_extractor` → `azure_openai_agent` → `text_summarizer` → `azure_blob_output` - -```yaml -name: contract-analysis -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: contracts - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - settings: - extract_text: true - extract_pages: true - - - executor: entity_extractor - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - entity_types: ["person", "organization", "date", "monetary_amount", "jurisdiction"] - output_field: entities - - - executor: azure_openai_agent - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - instructions: | - You are a legal contract analyst. Analyze the contract and extract: - 1. Key obligations per party - 2. Payment terms and amounts - 3. Termination clauses - 4. Governing law and jurisdiction - 5. Risk flags (unusual or one-sided clauses) - Respond in structured JSON. - input_field: pdf_output.text - output_field: contract_analysis - - - executor: text_summarizer - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - max_length: 400 - style: executive summary for legal review - input_field: pdf_output.text - output_field: summary - - - executor: azure_blob_output - settings: - blob_container_name: contracts-analyzed -``` - ---- - -### 7.4 Engineering PDF BOM Extraction - -**Goal**: Extract Bill of Materials (BOM) tables from engineering PDFs using Azure Document Intelligence, flag low-confidence line items (<90%), and write output to Azure Blob Storage. *(See fit assessment in previous analysis for implementation gap details.)* - -**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_document_intelligence_extractor` → `table_row_splitter` → `field_mapper` → [custom: `bom_excel_writer`] → [custom: `sharepoint_output`] - -```yaml -name: engineering-bom-extraction -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: engineering-pdfs - file_extensions: [".pdf"] - - - executor: azure_blob_content_retriever - settings: - use_temp_file_for_content: true - - - executor: azure_document_intelligence_extractor - settings: - doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" - model_id: prebuilt-layout - extract_tables: true - extract_text: false - output_field: doc_intell_output - - - executor: table_row_splitter - settings: - table_field: doc_intell_output.tables.0 - table_format: auto - has_header: true - skip_empty_rows: true - include_row_index: true - - - executor: field_mapper - settings: - mappings: - row_data.Item: item_number - row_data.Description: description - row_data.Qty: quantity - row_data.Unit: unit - row_data.Part_Number: part_number - - # Steps 6 and 7 require custom executors (not built-in): - # - bom_excel_writer: writes .xlsx with red fill on rows where confidence < 0.90 - # - sharepoint_output: writes file back to SharePoint via Microsoft Graph API - - - executor: azure_blob_output - settings: - blob_container_name: bom-output - output_format: json -``` - -**Custom executor gaps required for full Phase 1**: -- `bom_excel_writer` — writes formatted Excel with `openpyxl`, applies `PatternFill(red)` to rows where confidence < 0.90 -- `sharepoint_output` — uploads file to SharePoint document library via Microsoft Graph API -- Extend `DocumentIntelligenceConnector.extract_tables()` to expose `cell.confidence` from the SDK response - ---- - -### 7.5 Compliance & PII Detection - -**Goal**: Scan documents for PII, classify by sensitivity level, and generate compliance risk reports. - -**Key executors**: `pdf_extractor` / `word_extractor` → `pii_detector` → `content_classifier` → `azure_openai_agent` → `azure_blob_output` - -```yaml -name: compliance-scan -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documents-review - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - settings: - extract_text: true - - - executor: pii_detector - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - pii_types: ["name", "email", "phone", "ssn", "credit_card", "address", "dob"] - redact: false - output_field: pii_results - - - executor: content_classifier - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - categories: ["public", "internal", "confidential", "restricted"] - include_confidence: true - output_field: classification - - - executor: azure_openai_agent - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - instructions: | - Based on PII findings and document classification, assess regulatory exposure: - - GDPR, HIPAA, PCI-DSS applicability - - Risk level: low / medium / high / critical - - Required remediation actions - output_field: compliance_report - - - executor: azure_blob_output - settings: - blob_container_name: compliance-reports -``` - ---- - -### 7.6 Web Content Ingestion - -**Goal**: Crawl product documentation or support sites, chunk content, generate embeddings, and index in Azure AI Search for a RAG chatbot. - -**Key executors**: `web_scraper` → `recursive_text_chunker` → `language_detector` → `azure_openai_embeddings` → `ai_search_index_output` - -```yaml -name: web-content-ingestion -steps: - - executor: web_scraper - settings: - urls: - - "https://docs.company.com" - - "https://support.company.com" - max_depth: 3 - selectors: ["article", "main", ".content"] - output_field: scraped_content - - - executor: recursive_text_chunker - settings: - chunk_size: 1500 - chunk_overlap: 200 - input_field: scraped_content.text - - - executor: language_detector - settings: - input_field: scraped_content.text - output_field: detected_language - - - executor: azure_openai_embeddings - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: text-embedding-3-small - - - executor: ai_search_index_output - settings: - endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" - index_name: web-content-rag -``` - ---- - -### 7.7 Multilingual Document Translation - -**Goal**: Detect language of incoming documents, translate to target languages, and index translated content. - -**Key executors**: `pdf_extractor` → `language_detector` → `recursive_text_chunker` → `content_translator` → `azure_openai_embeddings` → `ai_search_index_output` - -```yaml -name: multilingual-translation -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: original-content - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - settings: - extract_text: true - - - executor: language_detector - settings: - input_field: pdf_output.text - output_field: source_language - - - executor: recursive_text_chunker - settings: - chunk_size: 2000 - input_field: pdf_output.text - - - executor: content_translator - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - target_languages: ["en", "es", "fr", "de", "pt"] - output_field: translations - - - executor: azure_openai_embeddings - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: text-embedding-3-small - - - executor: ai_search_index_output - settings: - endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" - index_name: multilingual-content -``` - ---- - -### 7.8 Email Attachment Triage - -**Goal**: Process email attachments, detect sentiment, extract action items and deadlines, classify by category, and route to the appropriate team queue. - -**Key executors**: `pdf_extractor` → `sentiment_analyser` → `entity_extractor` → `azure_openai_agent` → `content_classifier` → `azure_blob_output` - -```yaml -name: email-triage -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: email-attachments - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - settings: - extract_text: true - - - executor: sentiment_analyser - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - granularity: document - include_confidence: true - output_field: sentiment - - - executor: entity_extractor - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - entity_types: ["person", "date", "organization", "action_item"] - output_field: entities - - - executor: azure_openai_agent - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - instructions: | - Extract from this email: - 1. Action items and who owns them - 2. Deadlines mentioned - 3. Urgency level: low / medium / high / critical - 4. Subject matter summary in one sentence - output_field: triage_result - - - executor: content_classifier - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - categories: ["technical_support", "billing", "complaint", "legal", "general_inquiry"] - output_field: category - - - executor: azure_blob_output - settings: - blob_container_name: email-triaged -``` - ---- - -### 7.9 Spreadsheet Data Pipeline - -**Goal**: Process Excel files row by row, normalize field names, validate data, and export cleaned records to Blob Storage as JSON. - -**Key executors**: `content_retriever` → `excel_extractor` → `table_row_splitter` → `field_mapper` → `field_selector` → `azure_blob_output` - -```yaml -name: spreadsheet-pipeline -steps: - - executor: content_retriever - settings: - use_temp_file_for_content: true - - - executor: excel_extractor - settings: - extract_text: false - extract_sheets: true - extract_tables: true - first_row_as_header: true - skip_hidden_sheets: true - output_field: excel_output - - - executor: table_row_splitter - settings: - table_field: excel_output.sheets.0.table - table_format: auto - has_header: true - skip_empty_rows: true - include_row_index: true - - - executor: field_mapper - settings: - mappings: - row_data.CustomerID: customer_id - row_data.CustomerName: name - row_data.EmailAddress: email - row_data.PurchaseAmount: amount - - - executor: field_selector - settings: - include_fields: ["customer_id", "name", "email", "amount", "row_index"] - - - executor: azure_blob_output - settings: - blob_container_name: processed-data - output_format: json -``` - ---- - -### 7.10 Knowledge Graph Construction - -**Goal**: Extract entities and relationships from a corporate document corpus and build a structured knowledge graph for organizational intelligence. - -**Key executors**: `azure_document_intelligence_extractor` → `recursive_text_chunker` → `entity_extractor` → `azure_openai_agent` → `document_set_initializer` → `cross_document_comparison` → `azure_blob_output` - -```yaml -name: knowledge-graph -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: corporate-documents - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - settings: - doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" - model_id: prebuilt-layout - extract_text: true - - - executor: recursive_text_chunker - settings: - chunk_size: 2000 - chunk_overlap: 100 - input_field: doc_intell_output.text - - - executor: entity_extractor - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - entity_types: ["person", "organization", "product", "location", "project", "technology"] - output_field: entities - - - executor: azure_openai_agent - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - instructions: | - From the extracted entities, identify relationships such as: - works_at, manages, reports_to, located_in, provides, uses, competes_with - Return as structured JSON with relationship triples. - output_field: relationships - - - executor: azure_blob_output - settings: - blob_container_name: knowledge-graphs - output_format: json -``` - ---- - -## 8. Included Sample Pipelines - -The repository includes 28+ ready-to-run sample pipelines in `contentflow-lib/samples/`: - -| Folder | Sample Name | What it demonstrates | -|---|---|---| -| `01-simple/` | Simple pipeline | Minimal pipeline: content retrieval + Document Intelligence | -| `02-batch-processing/` | Batch processing | Processing multiple documents concurrently | -| `03-pdf-extractor_chunker/` | PDF + Chunking | PyMuPDF extraction + recursive text chunker | -| `04-word-extractor/` | Word extractor | `.docx` content extraction with metadata | -| `05-powerpoint-extractor/` | PowerPoint extractor | `.pptx` slide-by-slide extraction | -| `06-ai-analysis/` | AI analysis | GPT-4 analysis with configurable prompts | -| `07-embeddings/` | Embeddings | Azure OpenAI embedding generation | -| `08-content-understanding/` | Content Understanding | Azure Content Understanding prebuilt models | -| `09-blob-input/` | Blob input | Full blob discovery + retrieval + Document Intelligence | -| `10-table-row-splitter/` | Table row splitter | Splitting tables into per-row content items | -| `11-excel-extractor/` | Excel extractor | Full Excel workbook extraction | -| `12-field-transformation/` | Field transformation | Field mapping, selection, and normalization | -| `13-blob-output-sample/` | Blob output | Writing processed results to Blob Storage | -| `14-gpt-rag-ingestion/` | GPT-RAG ingestion | Complete end-to-end RAG knowledge base pipeline | -| `15-document-analysis/` | Document analysis | Full Document Intelligence analysis workflow | -| `16-spreadsheet-pipeline/` | Spreadsheet pipeline | Excel ingestion → row splitting → field normalization | -| `17-knowledge-graph/` | Knowledge graph | Entity + relationship extraction for graph construction | -| `18-web-scraping/` | Web scraping | Playwright-based crawling and content extraction | -| `19-sub-pipelines/` | Sub-pipelines | Pipeline composition and nested workflow execution | -| `20-document-set-static/` | Document set (static) | Fixed multi-document cross-analysis | -| `21-document-set-comparison/` | Document comparison | Side-by-side AI comparison of two documents | -| `22-document-set-dynamic/` | Document set (dynamic) | Dynamically assembled document sets | -| `23-inline-document-set/` | Inline document set | Document sets defined inline in the pipeline | -| `27-subpipeline-processing/` | Subpipeline processing | Advanced sub-pipeline patterns | -| `28-advanced-batch/` | Advanced batch | Complex batch processing with error handling | -| `32-parallel-processing/` | Parallel processing | Fan-out/fan-in with concurrent branches | -| `44-conditional-routing/` | Conditional routing | If/else branching based on content properties | - -**Parallel workflow patterns** (documented in `PARALLEL_WORKFLOWS_GUIDE.md`): -- `fan_out_fan_in.yaml` — Split into parallel branches, merge results -- `split_merge.yaml` — Split content list, process in parallel, collect results -- `batch_subworkflow_example.yaml` — Batch processing with nested subworkflows -- `conditional_routing.yaml` — Content-aware routing based on classification results - ---- - -*Document version: March 25, 2026 — Based on ContentFlow repository branch `local-documentation`* diff --git a/Analysis/01-overview.md b/Analysis/01-overview.md deleted file mode 100644 index 9a13436..0000000 --- a/Analysis/01-overview.md +++ /dev/null @@ -1,430 +0,0 @@ -# ContentFlow — Guía General de la Solución - -> **Plataforma inteligente de procesamiento de documentos y contenido construida sobre Azure.** - ---- - -## Índice - -- [1. ¿Qué es ContentFlow?](#1-qué-es-contentflow) -- [2. ¿Qué problema resuelve?](#2-qué-problema-resuelve) -- [3. Conceptos clave](#3-conceptos-clave) - - [3.1 Pipelines](#31-pipelines) - - [3.2 Executors (Ejecutores)](#32-executors-ejecutores) - - [3.3 Vaults (Bóvedas de documentos)](#33-vaults-bóvedas-de-documentos) - - [3.4 Modelo de Contenido](#34-modelo-de-contenido) -- [4. Componentes principales](#4-componentes-principales) - - [4.1 Interfaz Web (contentflow-web)](#41-interfaz-web-contentflow-web) - - [4.2 API REST (contentflow-api)](#42-api-rest-contentflow-api) - - [4.3 Workers de procesamiento (contentflow-worker)](#43-workers-de-procesamiento-contentflow-worker) - - [4.4 Librería de procesamiento (contentflow-lib)](#44-librería-de-procesamiento-contentflow-lib) - - [4.5 Infraestructura (infra)](#45-infraestructura-infra) -- [5. ¿Cómo funciona el flujo completo?](#5-cómo-funciona-el-flujo-completo) - - [5.1 Paso a paso: De un documento a datos inteligentes](#51-paso-a-paso-de-un-documento-a-datos-inteligentes) - - [5.2 Diagrama de flujo general](#52-diagrama-de-flujo-general) -- [6. Servicios de Azure utilizados](#6-servicios-de-azure-utilizados) -- [7. Modos de despliegue](#7-modos-de-despliegue) - - [7.1 Modo Básico](#71-modo-básico) - - [7.2 Modo AILZ (AI Landing Zone)](#72-modo-ailz-ai-landing-zone) -- [8. Ejemplo práctico: Pipeline de documentos PDF](#8-ejemplo-práctico-pipeline-de-documentos-pdf) -- [9. Resumen ejecutivo](#9-resumen-ejecutivo) - ---- - -## 1. ¿Qué es ContentFlow? - -**ContentFlow** es una plataforma empresarial cloud-native diseñada para transformar contenido no estructurado (PDFs, documentos Word, hojas de Excel, presentaciones PowerPoint, páginas web, etc.) en **datos inteligentes y accionables** mediante flujos de trabajo automatizados impulsados por servicios de inteligencia artificial de Azure. - -Piensa en ContentFlow como una **fábrica de procesamiento de documentos**: introduces documentos crudos por un extremo y, tras pasar por una serie de estaciones de procesamiento configurables (extracción, análisis, enriquecimiento, etc.), obtienes datos estructurados, indexados y listos para consumir. - ---- - -## 2. ¿Qué problema resuelve? - -Las organizaciones enfrentan desafíos comunes con su contenido: - -| Desafío | Cómo lo resuelve ContentFlow | -|---------|------------------------------| -| Documentos en múltiples formatos (PDF, Word, Excel...) | **Ejecutores de extracción** especializados para cada formato | -| Procesamiento manual y lento | **Pipelines automatizados** que procesan contenido sin intervención | -| Necesidad de análisis inteligente | **Integración con Azure AI** (GPT-4, Document Intelligence, embeddings) | -| Escalabilidad limitada | **Arquitectura distribuida** con workers paralelos y colas de mensajes | -| Dificultad para configurar flujos | **Editor visual** drag-and-drop + definición YAML | -| Seguridad empresarial | **Identidad administrada** (Managed Identity) sin contraseñas | - ---- - -## 3. Conceptos clave - -### 3.1 Pipelines - -Un **pipeline** es la unidad fundamental de trabajo en ContentFlow. Define una secuencia de pasos (ejecutores) que el contenido debe recorrer para ser procesado. Se definen en formato YAML: - -```yaml -name: procesar-pdfs -steps: - - executor: azure_blob_input_discovery # Descubre documentos - settings: - blob_container_name: documentos - - - executor: pdf_extractor # Extrae texto del PDF - - - executor: recursive_text_chunker # Divide en fragmentos - settings: - chunk_size: 1000 - - - executor: azure_openai_embeddings # Genera vectores (embeddings) - settings: - model: text-embedding-3-small - - - executor: azure_blob_output # Guarda resultados - settings: - blob_container_name: procesados -``` - -### 3.2 Executors (Ejecutores) - -Los **ejecutores** son las unidades de procesamiento individuales dentro de un pipeline. Cada ejecutor realiza una tarea específica. ContentFlow incluye **más de 35 ejecutores** organizados en categorías: - -| Categoría | Descripción | Ejemplos | -|-----------|-------------|----------| -| **Entrada** | Descubren contenido desde orígenes de datos | Blob Storage, sistema de archivos | -| **Extracción** | Parsean documentos y extraen texto | PDF, Word, Excel, PowerPoint, CSV | -| **Procesamiento IA** | Análisis inteligente con Azure AI | Embeddings, Document Intelligence, GPT-4 | -| **Transformación** | Manipulan y enriquecen datos | Chunking, mapeo de campos, agregación | -| **Salida** | Almacenan resultados procesados | Blob Storage, Azure AI Search | -| **Enrutamiento** | Lógica condicional y paralelismo | Fan-out/Fan-in, sub-pipelines | -| **Análisis de texto** | Procesamiento de lenguaje natural | Resumen, entidades, sentimiento, PII, idioma | -| **Especializado** | Tareas de dominio específico | Web scraping, grafos de conocimiento | - -### 3.3 Vaults (Bóvedas de documentos) - -Un **vault** es un contenedor lógico que agrupa documentos y los asocia a un pipeline específico. Cuando el vault está habilitado, los workers automáticamente descubren nuevos documentos y los procesan según el pipeline asociado. - -``` -Vault "Facturas 2024" - ├── Pipeline asociado: "procesar-facturas" - ├── Tags: [facturas, contabilidad] - ├── Documentos descubiertos: 1,247 - └── Estado: Habilitado ✓ -``` - -### 3.4 Modelo de Contenido - -Todo el contenido que fluye por un pipeline utiliza un **modelo estandarizado** (`Content`) que acumula datos a medida que pasa por cada ejecutor: - -``` -Content - ├── id: identificador único - ├── source: origen del documento (ej. blob://container/archivo.pdf) - ├── content: texto extraído - ├── metadata: información adicional del documento - ├── chunks: fragmentos de texto - ├── embeddings: vectores numéricos para búsqueda semántica - └── analysis: resultados de análisis IA -``` - ---- - -## 4. Componentes principales - -ContentFlow se compone de **5 componentes principales** que trabajan de forma coordinada: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ ContentFlow │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ │ -│ │ Web │──▶│ API │──▶│ Workers │──▶│ Librería │ │ -│ │ (React) │ │ (FastAPI)│ │ (Python) │ │ (Python) │ │ -│ └──────────┘ └────┬─────┘ └─────┬─────┘ └───────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌──────────────────────────────┐ │ -│ │ Infraestructura Azure │ │ -│ │ (Cosmos DB, Blob, Queue, │ │ -│ │ AI Services, Container │ │ -│ │ Apps, App Configuration) │ │ -│ └──────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 4.1 Interfaz Web (contentflow-web) - -La **interfaz web** es una aplicación React moderna que permite a los usuarios interactuar visualmente con ContentFlow: - -- **Constructor visual de pipelines**: Editor drag-and-drop basado en ReactFlow para diseñar pipelines gráficamente -- **Editor YAML**: Editor de código Monaco con resaltado de sintaxis para definir pipelines -- **Gestión de Vaults**: Crear, editar y monitorear bóvedas de documentos -- **Galería de plantillas**: Pipelines pre-construidos listos para usar -- **Panel de ejecuciones**: Ver el estado y resultados del procesamiento en tiempo real - -**Tecnologías**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn/ui, ReactFlow, Monaco Editor - -### 4.2 API REST (contentflow-api) - -El **servicio API** es el punto central de coordinación. Recibe solicitudes de la interfaz web y orquesta las operaciones: - -- **Gestión de pipelines**: Crear, leer, actualizar, eliminar y ejecutar pipelines -- **Gestión de vaults**: Administrar bóvedas de documentos -- **Catálogo de ejecutores**: Explorar los ejecutores disponibles y sus configuraciones -- **Verificación de salud**: Monitorear el estado de todos los servicios dependientes - -**Tecnologías**: FastAPI, Python 3.12+, Uvicorn, Pydantic, Azure SDK - -**Puerto**: 8090 - -### 4.3 Workers de procesamiento (contentflow-worker) - -Los **workers** son el motor de ejecución distribuida. Procesan el contenido de forma asíncrona utilizando múltiples procesos: - -- **Workers de entrada (Source Workers)**: Descubren contenido nuevo en los orígenes de datos configurados -- **Workers de procesamiento (Processing Workers)**: Ejecutan los pipelines sobre cada elemento de contenido - -**Funcionamiento simplificado**: -``` -Worker de Entrada Worker de Procesamiento - │ │ - ├── Lee vaults habilitados ├── Consulta la cola - ├── Ejecuta ejecutores de entrada ├── Recibe tarea - ├── Descubre documentos nuevos ├── Lee el pipeline - └── Crea tareas en la cola └── Ejecuta pipeline - sobre el contenido -``` - -**Tecnologías**: Python 3.12+, multiprocessing, Azure Storage Queue - -**Puerto**: 8099 (API de salud) - -### 4.4 Librería de procesamiento (contentflow-lib) - -La **librería** es el corazón técnico de ContentFlow. Contiene: - -- **Motor de pipelines**: Parsea YAML y ejecuta grafos dirigidos acíclicos (DAG) -- **+35 ejecutores**: Implementaciones listas para usar -- **Modelo de contenido**: Estructura de datos estandarizada -- **Conectores Azure**: Clientes para Blob, Cosmos DB, AI Search, OpenAI, Document Intelligence - -Esta librería se instala como dependencia en el API y el Worker. - -### 4.5 Infraestructura (infra) - -Plantillas **Bicep** (Infrastructure as Code) para aprovisionar todos los recursos de Azure necesarios. Se despliega con un solo comando `azd up`. - ---- - -## 5. ¿Cómo funciona el flujo completo? - -### 5.1 Paso a paso: De un documento a datos inteligentes - -``` - ① USUARIO ② API ③ COLA - ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ - │ Crea o edita│ ─────────▶ │ Almacena en │ ─────────▶ │ Encola │ - │ un pipeline │ REST API │ Cosmos DB y │ Storage │ tarea de │ - │ y ejecuta │ │ crea tarea │ Queue │ descubrir │ - └─────────────┘ └─────────────┘ └──────┬──────┘ - │ - ⑥ RESULTADOS ⑤ PROCESAMIENTO ④ DESCUBRIMIENTO - ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ - │ Datos en │ ◀───────── │ Ejecuta cada│ ◀───────── │ Worker lee │ - │ Blob Storage│ Guardado │ ejecutor del│ Cola de │ el origen y │ - │ y Cosmos DB │ │ pipeline │ tareas │ lista docs │ - └─────────────┘ └─────────────┘ └─────────────┘ -``` - -**Flujo detallado**: - -1. **El usuario** crea un pipeline (visualmente o en YAML) y lo ejecuta desde la interfaz web -2. **La API** recibe la solicitud, guarda la configuración en Cosmos DB y crea un registro de ejecución -3. **La API** envía una tarea de tipo `InputSource` a la cola de Azure Storage -4. **El Worker de Entrada** lee la tarea, ejecuta los ejecutores de entrada del pipeline y descubre los documentos disponibles -5. **Por cada documento** descubierto, el Worker de Entrada crea una tarea `ContentProcessing` en la cola -6. **Los Workers de Procesamiento** toman las tareas de la cola y ejecutan el pipeline completo (extracción → transformación → análisis → salida) sobre cada documento -7. **Los resultados** se almacenan en Blob Storage y/o Cosmos DB -8. **El usuario** puede monitorear el progreso y ver los resultados desde la interfaz web - -### 5.2 Diagrama de flujo general - -``` -┌─────────────┐ HTTP/REST ┌─────────────────┐ -│ │ ──────────────────▶ │ │ -│ Frontend │ │ API Service │ -│ (React) │ ◀────────────────── │ (FastAPI) │ -│ │ Respuestas │ │ -└─────────────┘ └───────┬─────────┘ - │ - ┌───────────────┼──────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌────────────┐ ┌────────────┐ ┌──────────────┐ - │ Cosmos DB │ │ Blob │ │ Storage │ - │ (Metadatos │ │ Storage │ │ Queue │ - │ y estado) │ │(Documentos)│ │ (Tareas) │ - └──────┬─────┘ └─────┬──────┘ └───────┬──────┘ - │ │ │ - ▼ ▼ ▼ - ┌─────────────────────────────────────────────────┐ - │ Worker Service │ - │ ┌─────────────────┐ ┌──────────────────────┐ │ - │ │ Source Workers │ │ Processing Workers │ │ - │ │ (Descubrimiento) │ │ (Ejecución pipeline) │ │ - │ └────────┬────────┘ └──────────┬───────────┘ │ - │ │ │ │ - │ ▼ ▼ │ - │ ┌────────────────────────────────────────────┐ │ - │ │ contentflow-lib │ │ - │ │ (Motor de pipelines + 35+ Ejecutores) │ │ - │ └────────────────────┬───────────────────────┘ │ - └───────────────────────┼─────────────────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Azure AI Services │ - │ ┌──────┐ ┌──────────┐ │ - │ │GPT-4 │ │ Document │ │ - │ │OpenAI│ │ Intelli- │ │ - │ │ │ │ gence │ │ - │ └──────┘ └──────────┘ │ - └──────────────────────────┘ -``` - ---- - -## 6. Servicios de Azure utilizados - -| Servicio de Azure | Propósito en ContentFlow | SKU / Nivel | -|-------------------|--------------------------|-------------| -| **Azure Container Apps** | Hospeda los 3 servicios (API, Worker, Web) | Consumption (serverless) | -| **Azure Cosmos DB** | Base de datos para metadatos, pipelines, ejecuciones | Serverless | -| **Azure Blob Storage** | Almacena documentos y contenido procesado | Standard_LRS, Hot | -| **Azure Storage Queue** | Cola de mensajes para distribución de tareas | Incluido en Storage | -| **Azure App Configuration** | Configuración centralizada de todos los servicios | Standard | -| **Azure Container Registry** | Registro de imágenes Docker | Standard (Premium con endpoints privados) | -| **Azure Log Analytics** | Workspace de logs y telemetría | PerGB2018 | -| **Azure Application Insights** | Monitoreo y diagnósticos | Conectado a Log Analytics | -| **Azure AI Foundry** | Plataforma de modelos de IA | S0 | -| **GPT-4.1 / GPT-4.1-mini** | Modelos de lenguaje para análisis inteligente | GlobalStandard, Capacidad: 100 | -| **Azure Document Intelligence** | Extracción inteligente de documentos (OCR) | Según integración | -| **Managed Identity** | Autenticación sin contraseñas entre servicios | User-Assigned | - ---- - -## 7. Modos de despliegue - -ContentFlow soporta dos modos de despliegue para adaptarse a diferentes necesidades: - -### 7.1 Modo Básico - -Ideal para **desarrollo, pruebas y demos**: - -- Endpoints públicos accesibles desde internet -- Red virtual creada automáticamente -- Todos los recursos se crean nuevos -- Sin endpoints privados -- Despliegue rápido y sencillo - -```bash -DEPLOYMENT_MODE=basic -azd up -``` - -### 7.2 Modo AILZ (AI Landing Zone) - -Diseñado para **entornos de producción empresarial**: - -- Se integra con una Azure AI Landing Zone existente -- Endpoints privados para todos los servicios -- Red virtual compartida con subnets dedicadas -- Zonas DNS privadas para resolución interna -- Cumplimiento con estándares de seguridad empresarial - -```bash -DEPLOYMENT_MODE=ailz-integrated -azd up -``` - -``` -┌─────────────────────────────────────────────────────────┐ -│ AI Landing Zone VNet │ -│ │ -│ ┌──────────────────┐ ┌──────────────────────────┐ │ -│ │ aca-env-subnet │ │ pe-subnet │ │ -│ │ ┌────────────┐ │ │ ┌──────┐ ┌──────────┐ │ │ -│ │ │ Container │ │ │ │Cosmos│ │ Storage │ │ │ -│ │ │ Apps (API, │ │ │ │ DB │ │ Account │ │ │ -│ │ │ Worker,Web)│ │ │ │(PE) │ │ (PE) │ │ │ -│ │ └────────────┘ │ │ └──────┘ └──────────┘ │ │ -│ └──────────────────┘ └──────────────────────────┘ │ -│ │ -│ PE = Private Endpoint │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## 8. Ejemplo práctico: Pipeline de documentos PDF - -A continuación, un ejemplo concreto de cómo ContentFlow procesa un conjunto de documentos PDF para crear un índice de búsqueda inteligente: - -**Pipeline: "Indexación de documentos PDF"** - -```yaml -name: indexacion-pdf -steps: - # 1. Descubrir PDFs en Blob Storage - - executor: azure_blob_input_discovery - settings: - blob_container_name: documentos-legales - file_extensions: [".pdf"] - - # 2. Recuperar contenido del blob - - executor: azure_blob_content_retriever - - # 3. Extraer texto con Azure Document Intelligence - - executor: azure_document_intelligence_extractor - settings: - model_id: prebuilt-layout - - # 4. Dividir en fragmentos manejables - - executor: recursive_text_chunker - settings: - chunk_size: 1000 - chunk_overlap: 200 - - # 5. Generar vectores para búsqueda semántica - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - # 6. Indexar en Azure AI Search - - executor: ai_search_index_output - settings: - index_name: documentos-legales-index -``` - -**Resultado**: Cada PDF se convierte en fragmentos indexados con vectores semánticos, listos para búsquedas inteligentes tipo "¿cuáles son las cláusulas de rescisión?". - ---- - -## 9. Resumen ejecutivo - -| Aspecto | Detalle | -|---------|---------| -| **Tipo de solución** | Plataforma de procesamiento de contenido empresarial | -| **Arquitectura** | Microservicios, event-driven, cloud-native | -| **Nube** | Microsoft Azure | -| **Servicios** | 3 contenedores: API, Worker, Web | -| **Motor de IA** | GPT-4.1, Document Intelligence, Embeddings | -| **Base de datos** | Cosmos DB (Serverless) | -| **Almacenamiento** | Azure Blob Storage | -| **Mensajería** | Azure Storage Queue | -| **Ejecutores disponibles** | 35+ ejecutores pre-construidos | -| **Formatos soportados** | PDF, Word, Excel, PowerPoint, CSV, HTML, web | -| **Seguridad** | Managed Identity, RBAC, sin contraseñas | -| **Despliegue** | `azd up` — un solo comando | -| **Modos** | Básico (desarrollo) o AILZ (producción empresarial) | - ---- - -> **ContentFlow** simplifica el camino desde documentos crudos hasta datos inteligentes, combinando la potencia de Azure AI con una arquitectura escalable y una experiencia de usuario intuitiva. diff --git a/Analysis/02-architecture-detailed.md b/Analysis/02-architecture-detailed.md deleted file mode 100644 index a3f9f6e..0000000 --- a/Analysis/02-architecture-detailed.md +++ /dev/null @@ -1,1920 +0,0 @@ -# ContentFlow — Arquitectura Detallada de Componentes - -> **Documentación técnica completa**: componentes, interconexiones, flujos de datos, SKUs, configuración y diagramas de la plataforma ContentFlow. - ---- - -## Índice - -- [1. Visión general de la arquitectura](#1-visión-general-de-la-arquitectura) - - [1.1 Diagrama de arquitectura completa](#11-diagrama-de-arquitectura-completa) - - [1.2 Principios arquitectónicos](#12-principios-arquitectónicos) -- [2. Componente: Interfaz Web (contentflow-web)](#2-componente-interfaz-web-contentflow-web) - - [2.1 Stack tecnológico](#21-stack-tecnológico) - - [2.2 Estructura de la aplicación](#22-estructura-de-la-aplicación) - - [2.3 Rutas y navegación](#23-rutas-y-navegación) - - [2.4 Componentes principales de la UI](#24-componentes-principales-de-la-ui) - - [2.5 Gestión de estado y datos](#25-gestión-de-estado-y-datos) - - [2.6 Diagrama de componentes Web](#26-diagrama-de-componentes-web) -- [3. Componente: API REST (contentflow-api)](#3-componente-api-rest-contentflow-api) - - [3.1 Stack tecnológico](#31-stack-tecnológico) - - [3.2 Endpoints del API](#32-endpoints-del-api) - - [3.3 Capa de servicios](#33-capa-de-servicios) - - [3.4 Capa de base de datos](#34-capa-de-base-de-datos) - - [3.5 Modelos de datos](#35-modelos-de-datos) - - [3.6 Inyección de dependencias](#36-inyección-de-dependencias) - - [3.7 Configuración y arranque](#37-configuración-y-arranque) - - [3.8 Diagrama de capas del API](#38-diagrama-de-capas-del-api) -- [4. Componente: Worker Service (contentflow-worker)](#4-componente-worker-service-contentflow-worker) - - [4.1 Stack tecnológico](#41-stack-tecnológico) - - [4.2 Arquitectura multi-proceso](#42-arquitectura-multi-proceso) - - [4.3 Worker de entrada (Input Source Worker)](#43-worker-de-entrada-input-source-worker) - - [4.4 Worker de procesamiento (Content Processing Worker)](#44-worker-de-procesamiento-content-processing-worker) - - [4.5 Cliente de cola](#45-cliente-de-cola) - - [4.6 Ciclo de vida de una tarea](#46-ciclo-de-vida-de-una-tarea) - - [4.7 Configuración del Worker](#47-configuración-del-worker) - - [4.8 Diagrama del motor de workers](#48-diagrama-del-motor-de-workers) -- [5. Componente: Librería Core (contentflow-lib)](#5-componente-librería-core-contentflow-lib) - - [5.1 Motor de pipelines](#51-motor-de-pipelines) - - [5.2 Jerarquía de clases de ejecutores](#52-jerarquía-de-clases-de-ejecutores) - - [5.3 Catálogo completo de ejecutores](#53-catálogo-completo-de-ejecutores) - - [5.4 Modelo de contenido (Content)](#54-modelo-de-contenido-content) - - [5.5 Conectores de Azure](#55-conectores-de-azure) - - [5.6 Diagrama del motor de pipelines](#56-diagrama-del-motor-de-pipelines) -- [6. Infraestructura de Azure (infra)](#6-infraestructura-de-azure-infra) - - [6.1 Recursos desplegados](#61-recursos-desplegados) - - [6.2 SKUs y niveles de servicio](#62-skus-y-niveles-de-servicio) - - [6.3 Módulos Bicep](#63-módulos-bicep) - - [6.4 Cosmos DB — Estructura de base de datos](#64-cosmos-db--estructura-de-base-de-datos) - - [6.5 Storage Account — Blobs y colas](#65-storage-account--blobs-y-colas) - - [6.6 Container Apps — Configuración de servicios](#66-container-apps--configuración-de-servicios) - - [6.7 AI Foundry — Modelos de IA](#67-ai-foundry--modelos-de-ia) - - [6.8 Diagrama de infraestructura Azure](#68-diagrama-de-infraestructura-azure) -- [7. Interconexión entre componentes](#7-interconexión-entre-componentes) - - [7.1 Flujo de datos completo](#71-flujo-de-datos-completo) - - [7.2 Flujo de mensajes por cola](#72-flujo-de-mensajes-por-cola) - - [7.3 Flujo de autenticación](#73-flujo-de-autenticación) - - [7.4 Matriz de comunicación entre servicios](#74-matriz-de-comunicación-entre-servicios) - - [7.5 Diagrama de secuencia: Ejecución de pipeline](#75-diagrama-de-secuencia-ejecución-de-pipeline) -- [8. Modos de despliegue](#8-modos-de-despliegue) - - [8.1 Modo básico](#81-modo-básico) - - [8.2 Modo AILZ (AI Landing Zone)](#82-modo-ailz-ai-landing-zone) - - [8.3 Proceso de despliegue con azd](#83-proceso-de-despliegue-con-azd) - - [8.4 Diagrama comparativo de modos](#84-diagrama-comparativo-de-modos) -- [9. Seguridad y autenticación](#9-seguridad-y-autenticación) - - [9.1 Modelo de identidad](#91-modelo-de-identidad) - - [9.2 Roles RBAC asignados](#92-roles-rbac-asignados) - - [9.3 Diagrama de seguridad](#93-diagrama-de-seguridad) -- [10. Configuración centralizada](#10-configuración-centralizada) - - [10.1 Azure App Configuration](#101-azure-app-configuration) - - [10.2 Variables de entorno por servicio](#102-variables-de-entorno-por-servicio) -- [11. Monitoreo y observabilidad](#11-monitoreo-y-observabilidad) -- [12. Resumen de dependencias entre componentes](#12-resumen-de-dependencias-entre-componentes) - ---- - -## 1. Visión general de la arquitectura - -ContentFlow implementa una **arquitectura de microservicios event-driven (basada en eventos)** con los siguientes patrones clave: - -- **Microservicios**: 3 servicios independientes (API, Worker, Web) desplegados como contenedores -- **Event-Driven**: Comunicación asíncrona mediante colas de mensajes -- **Cloud-Native**: Diseñado para Azure Container Apps con escalado automático -- **Declarativo**: Pipelines definidos en YAML, infraestructura definida en Bicep - -### 1.1 Diagrama de arquitectura completa - -``` -┌──────────────────────────────────────────────────────────────────────────────────┐ -│ AZURE CONTAINER APPS ENVIRONMENT │ -│ │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ contentflow-web │ │ contentflow-api │ │contentflow-worker│ │ -│ │ │ │ │ │ │ │ -│ │ React 18 │ │ FastAPI │ │ Multi-process │ │ -│ │ TypeScript │───▶│ Python 3.12+ │ │ Python 3.12+ │ │ -│ │ Vite │ │ Puerto: 8090 │ │ Puerto: 8099 │ │ -│ │ ReactFlow │ │ │ │ │ │ -│ │ Monaco Editor │ │ ┌────────────┐ │ │ ┌──────────────┐ │ │ -│ │ │ │ │ Routers │ │ │ │Source Workers│ │ │ -│ │ │ │ │ Services │ │ │ │Process Worker│ │ │ -│ │ │ │ │ Models │ │ │ │Queue Client │ │ │ -│ │ │ │ └────────────┘ │ │ └──────────────┘ │ │ -│ │ CPU: 1 | 2Gi │ │ CPU: 1 | 2Gi │ │ CPU: 1 | 2Gi │ │ -│ │ Réplicas: 1-2 │ │ Réplicas: 1-2 │ │ Réplicas: 1-2 │ │ -│ └──────────────────┘ └───────┬──────────┘ └────────┬─────────┘ │ -│ │ │ │ -└──────────────────────────────────┼─────────────────────────┼─────────────────────┘ - │ │ - ┌───────────────────────┼─────────────────────────┼──────────────┐ - │ ▼ ▼ │ - │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │ - │ │ Cosmos DB │ │ Blob │ │ Storage Queue │ │ - │ │ (Serverless)│ │ Storage │ │ (contentflow- │ │ - │ │ │ │ (Standard │ │ execution-requests) │ │ - │ │ 7 containers│ │ LRS, Hot) │ │ │ │ - │ └─────────────┘ └─────────────┘ └──────────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │ - │ │ App Config │ │ App Insights │ │ AI Foundry (S0) │ │ - │ │ (Standard) │ │ + Log Analyt.│ │ GPT-4.1 │ │ - │ │ │ │ (PerGB2018) │ │ GPT-4.1-mini │ │ - │ └──────────────┘ └──────────────┘ └────────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────┐ │ - │ │ Container │ │ Managed │ │ - │ │ Registry │ │ Identity │ │ - │ │ (Standard) │ │ (User Assign)│ │ - │ └──────────────┘ └──────────────┘ │ - │ │ - │ SERVICIOS DE AZURE │ - └────────────────────────────────────────────────────────────────┘ -``` - -### 1.2 Principios arquitectónicos - -| Principio | Implementación | -|-----------|---------------| -| **Desacoplamiento** | Los servicios se comunican a través de colas y base de datos compartida, no llamadas directas | -| **Escalabilidad horizontal** | Workers pueden escalarse independientemente (1-2 réplicas con autoescalado) | -| **Procesamiento asíncrono** | Las tareas se encolan y procesan en segundo plano | -| **Resiliencia** | Reintentos configurables (3 por defecto), timeouts, y manejo de errores por ejecutor | -| **Seguridad Zero-Trust** | Managed Identity, sin contraseñas compartidas, RBAC para cada recurso | -| **Configuración centralizada** | Azure App Configuration como fuente única de verdad | -| **Observabilidad** | Application Insights + Log Analytics para trazabilidad distribuida | - ---- - -## 2. Componente: Interfaz Web (contentflow-web) - -### 2.1 Stack tecnológico - -| Tecnología | Versión | Rol | -|------------|---------|-----| -| React | 18.3.1 | Framework UI | -| TypeScript | 5.8.3 | Lenguaje tipado | -| Vite | 7.1.5 | Empaquetador y servidor de desarrollo | -| Tailwind CSS | 3.4.17 | Framework de estilos utilitarios | -| ReactFlow | 11.11.4 | Visualización de grafos (pipeline builder) | -| Monaco Editor | 4.7.0 | Editor de código YAML | -| TanStack Query | 5.83.0 | Gestión de estado del servidor | -| React Hook Form | 7.61.1 | Formularios con validación | -| Zod | 3.25.76 | Esquemas de validación TypeScript | -| Shadcn/ui + Radix UI | Múltiples | Componentes UI accesibles | -| Recharts | 2.15.4 | Gráficas y visualizaciones | -| Lucide React | 0.462.0 | Iconografía | -| js-yaml | 4.1.1 | Parsing YAML | -| date-fns | 3.6.0 | Utilidades de fecha | - -### 2.2 Estructura de la aplicación - -``` -contentflow-web/src/ -├── main.tsx # Punto de entrada de React -├── App.tsx # Componente raíz con providers y rutas -├── index.css # Estilos globales (Tailwind) -├── components/ -│ ├── ui/ # Componentes base Shadcn/ui -│ │ ├── button.tsx # Botones -│ │ ├── dialog.tsx # Diálogos modales -│ │ ├── input.tsx # Campos de entrada -│ │ ├── select.tsx # Selectores -│ │ ├── toast.tsx # Notificaciones -│ │ ├── tabs.tsx # Pestañas -│ │ ├── card.tsx # Tarjetas -│ │ └── ... # +20 componentes más -│ ├── pipeline/ # Componentes del constructor de pipelines -│ ├── vaults/ # Componentes de gestión de vaults -│ ├── dashboard/ # Componentes del panel principal -│ ├── knowledge/ # Componentes del grafo de conocimiento -│ ├── templates/ # Componentes de plantillas -│ ├── Hero.tsx # Sección de bienvenida -│ ├── PipelineBuilder.tsx # Constructor visual de pipelines -│ ├── KnowledgeGraph.tsx # Visualizador de grafos -│ ├── Footer.tsx # Pie de página -│ └── NavLink.tsx # Enlace de navegación -├── pages/ -│ ├── Index.tsx # Página principal (home, pipeline, vaults) -│ ├── Templates.tsx # Galería de plantillas -│ ├── Vaults.tsx # Gestión de vaults (ruta directa) -│ └── NotFound.tsx # Página 404 -├── hooks/ # Custom hooks de React -├── types/ # Definiciones de tipos TypeScript -├── lib/ # Utilidades y configuración (cn, utils) -└── data/ # Datos mock y constantes -``` - -### 2.3 Rutas y navegación - -| Ruta | Componente | Vista | Descripción | -|------|-----------|-------|-------------| -| `/` | Index | home | Página principal con hero y navegación | -| `/?view=pipeline` | Index | pipeline | Constructor visual de pipelines | -| `/?view=graph` | Index | graph | Visualizador de grafos de conocimiento | -| `/?view=vaults` | Index | vaults | Gestión de bóvedas de documentos | -| `/templates` | Templates | — | Galería de plantillas pre-construidas | -| `/*` | NotFound | — | Página 404 | - -### 2.4 Componentes principales de la UI - -``` -┌────────────────────────────────────────────────────────────────┐ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Barra de Navegación │ │ -│ │ [Logo] ContentFlow | Home | Pipeline | Vaults | │ │ -│ │ Templates │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ CONTENIDO PRINCIPAL (según vista activa) │ │ -│ │ │ │ -│ │ ┌─ Home ──────────────────────────────────────────┐ │ │ -│ │ │ Hero: Bienvenida y métricas del sistema │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌─ Pipeline Builder ──────────────────────────────┐ │ │ -│ │ │ ┌───────────┐ ┌───────────────────────────┐ │ │ │ -│ │ │ │ Catálogo │ │ Lienzo ReactFlow │ │ │ │ -│ │ │ │ de │ │ (Nodos + Aristas) │ │ │ │ -│ │ │ │ ejecutores│ │ │ │ │ │ -│ │ │ │ │ │ [Nodo A] ──▶ [Nodo B] │ │ │ │ -│ │ │ │ • Input │ │ │ │ │ │ │ -│ │ │ │ • Extract │ │ ▼ │ │ │ │ -│ │ │ │ • Process │ │ [Nodo C] ──▶ [Nodo D] │ │ │ │ -│ │ │ │ • Output │ │ │ │ │ │ -│ │ │ └───────────┘ └───────────────────────────┘ │ │ │ -│ │ │ ┌─────────────────────────────────────────────┐│ │ │ -│ │ │ │ Editor YAML (Monaco) ││ │ │ -│ │ │ └─────────────────────────────────────────────┘│ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌─ Vaults ────────────────────────────────────────┐ │ │ -│ │ │ Lista de bóvedas │ Detalle │ Ejecuciones │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Footer │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└────────────────────────────────────────────────────────────────┘ -``` - -### 2.5 Gestión de estado y datos - -| Librería | Responsabilidad | Patrón | -|----------|----------------|--------| -| **TanStack Query** | Estado del servidor (datos de la API) | Cache con invalidación automática | -| **React Hook Form** | Estado de formularios | Formularios controlados con validación | -| **Zod** | Validación de esquemas | Validación en tiempo de ejecución | -| **URL Query Params** | Navegación y vista activa | Estado en la URL (ej. `?view=pipeline`) | -| **React Context** | Estado global de la app (si aplica) | Provider pattern | - -### 2.6 Diagrama de componentes Web - -``` - App.tsx - │ - ┌───────────┼───────────────┐ - │ │ │ - QueryClient BrowserRouter Providers - Provider │ (Tooltip, - │ │ Toast, - │ ┌─────┴──────┐ Sonner) - │ │ │ - │ Route / Route /templates - │ │ │ - │ Index Templates - │ │ - │ ├── Hero - │ ├── PipelineBuilder ──▶ ReactFlow + Monaco - │ ├── KnowledgeGraph ──▶ Visualización de grafos - │ └── Vaults ──▶ CRUD de bóvedas - │ - └── Comunicación con API vía fetch/httpx - (GET/POST/PUT/DELETE → /api/*) -``` - ---- - -## 3. Componente: API REST (contentflow-api) - -### 3.1 Stack tecnológico - -| Tecnología | Versión | Rol | -|------------|---------|-----| -| Python | 3.12+ | Runtime | -| FastAPI | 0.128.0 | Framework web asíncrono | -| Uvicorn | 0.40.0 | Servidor ASGI | -| Pydantic | 2.12+ | Validación de datos y modelos | -| pydantic-settings | 2.12.0 | Gestión de configuración | -| azure-identity | 1.25.1 | Autenticación con Managed Identity | -| azure-cosmos | 4.14.3 | Cliente Cosmos DB NoSQL | -| azure-storage-blob | 12.27.1 | Cliente Blob Storage | -| azure-storage-queue | 12.14.1 | Cliente Storage Queue | -| azure-appconfiguration-provider | 2.3.1 | Configuración centralizada | -| httpx | 0.28.1 | Cliente HTTP asíncrono | -| aiohttp | 3.13.3+ | Cliente HTTP asíncrono alternativo | -| PyYAML | 6.0.3+ | Parsing YAML | -| python-dotenv | 1.2.1+ | Variables de entorno locales | -| contentflow-lib | local | Librería de procesamiento (editable) | - -### 3.2 Endpoints del API - -#### Salud del sistema - -| Método | Ruta | Descripción | Respuesta | -|--------|------|-------------|-----------| -| `GET` | `/api/health/` | Verificación completa de todos los servicios | `SystemHealth` | -| `GET` | `/api/health/{service}` | Verificar un servicio específico | `ServiceHealth` | - -Servicios verificados: `cosmos_db`, `storage_queue`, `app_config` - -#### Gestión de Pipelines - -| Método | Ruta | Descripción | Request Body | Respuesta | -|--------|------|-------------|-------------|-----------| -| `GET` | `/api/pipelines/` | Listar todos los pipelines | — | `List[Pipeline]` | -| `GET` | `/api/pipelines/{id_or_name}` | Obtener por ID o nombre | — | `Pipeline` | -| `POST` | `/api/pipelines/` | Crear o actualizar pipeline | `SavePipelineRequest` | `Pipeline` | -| `DELETE` | `/api/pipelines/{id}` | Eliminar pipeline | — | `bool` | -| `POST` | `/api/pipelines/{id}/execute` | Ejecutar pipeline | `ExecutePipelineRequest` | `ExecutePipelineResponse` | - -#### Gestión de Vaults - -| Método | Ruta | Descripción | Parámetros | Respuesta | -|--------|------|-------------|-----------|-----------| -| `GET` | `/api/vaults/` | Listar vaults | `search`, `tags` | `List[Vault]` | -| `GET` | `/api/vaults/{id}` | Obtener vault por ID | — | `Vault` | -| `POST` | `/api/vaults/` | Crear vault | Body: `VaultCreateRequest` | `Vault` | -| `PUT` | `/api/vaults/{id}` | Actualizar vault | Body: `VaultUpdateRequest` | `Vault` | -| `DELETE` | `/api/vaults/{id}` | Eliminar vault | — | `bool` | - -#### Catálogo de Ejecutores - -| Método | Ruta | Descripción | Respuesta | -|--------|------|-------------|-----------| -| `GET` | `/api/executors/` | Listar todos los ejecutores | `List[ExecutorCatalogDefinition]` | -| `GET` | `/api/executors/{id}` | Obtener ejecutor por ID | `ExecutorCatalogDefinition` | - -#### Raíz - -| Método | Ruta | Descripción | -|--------|------|-------------| -| `GET` | `/` | Mensaje de bienvenida y versión del API | - -### 3.3 Capa de servicios - -Cada servicio implementa lógica de negocio específica heredando de `BaseService`: - -``` -BaseService (CRUD genérico sobre Cosmos DB) - │ - ├── PipelineService - │ ├── get_pipelines() → List[Pipeline] - │ ├── get_pipeline_by_id(id) → Pipeline - │ ├── get_pipeline_by_name(name) → Pipeline - │ ├── create_pipeline(data) → Pipeline - │ ├── create_or_save_pipeline(data) → Pipeline - │ ├── update_by_id(id, data) → Pipeline - │ └── delete_pipeline_by_id(id) → bool - │ - ├── VaultService - │ ├── create_vault(request, pipeline_name) → Vault - │ ├── get_vault(id) → Vault - │ ├── list_vaults(search, tags) → List[Vault] - │ ├── update_vault(id, request) → Vault - │ └── delete_vault(id) → bool - │ - ├── VaultExecutionService - │ ├── get_vault_executions(vault_id, start, end) → List[VaultExecution] - │ ├── get_vault_crawl_checkpoints(vault_id) → List[Checkpoint] - │ ├── delete_vault_executions(vault_id) - │ └── delete_vault_crawl_checkpoints(vault_id) - │ - ├── PipelineExecutionService - │ └── Seguimiento de ejecuciones de pipeline - │ - ├── ExecutorCatalogService - │ └── Gestión del catálogo de ejecutores - │ - └── HealthService - ├── check_all_services() → SystemHealth - └── check_service_health(name) → ServiceHealth -``` - -### 3.4 Capa de base de datos - -**CosmosDBClient** — Wrapper asíncrono sobre el SDK oficial de Azure Cosmos DB: - -``` -CosmosDBClient - │ - ├── connect() # Inicializar conexión con credenciales - ├── close() # Cerrar conexión - │ - ├── create(container, item) # Crear documento - ├── get_by_id(container, id) # Obtener por ID - ├── list_all(container, query) # Listar con consulta SQL - ├── query(container, query, params) # Consulta parametrizada - ├── update(container, item) # Upsert (crear o actualizar) - ├── delete(container, id) # Eliminar - └── batch_upsert(container, items) # Actualización masiva -``` - -**Características**: -- Autenticación via `ChainedTokenCredential` (Managed Identity) -- Creación automática de la base de datos si no existe -- Consultas cross-partition habilitadas -- Pool de conexiones compartido - -### 3.5 Modelos de datos - -``` -CosmosBaseModel (Base) - │ - ├── id: str (UUID v4 auto-generado) - ├── created_at: datetime - └── modified_at: datetime - -Pipeline (Definición de pipeline) - ├── name: str - ├── description: str - ├── yaml: str (definición YAML del pipeline) - ├── nodes: List[Any] (nodos del grafo visual) - ├── edges: List[Any] (aristas del grafo visual) - ├── tags: List[str] - ├── enabled: bool - ├── retry_delay: int (segundos, default: 5) - ├── timeout: int (segundos, default: 600) - ├── retries: int (default: 3) - └── version: str - -PipelineExecution (Ejecución de pipeline) - ├── pipeline_id: str - ├── pipeline_name: str - ├── status: ExecutionStatus (pending|running|completed|failed|cancelled) - ├── inputs: Dict - ├── configuration: Dict - ├── outputs: Dict - ├── started_at: str (ISO) - ├── completed_at: str (ISO) - ├── duration_seconds: float - ├── error: str - ├── executor_outputs: Dict[str, ExecutorOutput] - └── events: List[PipelineExecutionEvent] - -Vault (Bóveda de documentos) - ├── name: str (1-100 caracteres) - ├── description: str (max 500) - ├── pipeline_id: str - ├── pipeline_name: str - ├── tags: List[str] - ├── save_execution_output: bool - ├── enabled: bool - └── created_by: str - -VaultExecution (Ejecución de vault) - ├── pipeline_id / vault_id: str - ├── status: pending|running|completed|failed - ├── task_id: str - ├── source_worker_id / processing_worker_id: str - ├── executor_outputs: Dict - ├── events: List[Dict] - ├── content: List[Dict] - └── number_of_items: int - -VaultCrawlCheckpoint (Punto de control de rastreo) - ├── pipeline_id / vault_id / executor_id: str - ├── checkpoint_timestamp: str - └── worker_id: str -``` - -### 3.6 Inyección de dependencias - -FastAPI utiliza inyección de dependencias para proporcionar clientes compartidos a los routers: - -``` -get_config_provider() ──▶ ConfigurationProvider (Azure App Config) - │ │ - ▼ ▼ -get_cosmos_client() ──▶ CosmosDBClient (singleton) - │ - ├──▶ get_pipeline_service() ──▶ PipelineService - ├──▶ get_vault_service() ──▶ VaultService - ├──▶ get_vault_execution_service() ──▶ VaultExecutionService - ├──▶ get_executor_catalog_service() ──▶ ExecutorCatalogService - ├──▶ get_pipeline_execution_service() ──▶ PipelineExecutionService - └──▶ get_health_service() ──▶ HealthService - -Todas las dependencias usan un TTL de caché de 10 minutos. -``` - -### 3.7 Configuración y arranque - -**Fuentes de configuración** (en orden de prioridad): - -1. **Variables de entorno** (ej. `.env` para desarrollo local) -2. **Azure App Configuration** (producción) -3. **Valores por defecto** (hardcoded en `settings.py`) - -**Secuencia de arranque del API**: - -``` -main.py - │ - ├── 1. Crear aplicación FastAPI - ├── 2. Configurar CORS (allow_origins: ["*"]) - ├── 3. Registrar routers (/api/health, /api/pipelines, /api/vaults, /api/executors) - │ - └── Lifespan (async context manager): - ├── STARTUP: - │ ├── initialize_cosmos() → Conectar a Cosmos DB - │ ├── initialize_blob_storage() → Inicializar Blob Storage - │ └── initialize_executor_catalog() → Cargar catálogo de ejecutores - │ - └── SHUTDOWN: - └── Limpieza de recursos -``` - -**Configuración del servidor**: - -| Parámetro | Valor | Descripción | -|-----------|-------|-------------| -| Host | `0.0.0.0` | Escuchar en todas las interfaces | -| Puerto | `8090` | Puerto HTTP | -| Workers | `1` | Un proceso Uvicorn | -| Reload | `True` (en DEBUG) | Recarga automática en desarrollo | -| OpenAPI docs | `/docs` | Documentación interactiva Swagger | -| ReDoc | `/redoc` | Documentación alternativa | - -### 3.8 Diagrama de capas del API - -``` -┌─────────────────────────────────────────────────────────┐ -│ CAPA DE PRESENTACIÓN │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ -│ │ /health │ │/pipelines│ │ /vaults │ │/executors │ │ -│ │ Router │ │ Router │ │ Router │ │ Router │ │ -│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ -│ │ │ │ │ │ -├───────┼────────────┼────────────┼──────────────┼─────────┤ -│ ▼ ▼ ▼ ▼ │ -│ CAPA DE SERVICIOS │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ -│ │ Health │ │ Pipeline │ │ Vault │ │ Catalog │ │ -│ │ Service │ │ Service │ │ Service │ │ Service │ │ -│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ -│ │ │ │ │ │ -├───────┼────────────┼────────────┼──────────────┼─────────┤ -│ ▼ ▼ ▼ ▼ │ -│ CAPA DE DATOS │ -│ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ CosmosDBClient (Async) │ │ -│ │ ┌─────────────────────────────────────────────┐ │ │ -│ │ │ ChainedTokenCredential (Managed Identity) │ │ │ -│ │ └─────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ -│ │ BlobStorage │ │ StorageQueue │ │ AppConfig │ │ -│ │ Client │ │ Client │ │ Provider │ │ -│ └──────────────┘ └──────────────┘ └───────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## 4. Componente: Worker Service (contentflow-worker) - -### 4.1 Stack tecnológico - -| Tecnología | Versión | Rol | -|------------|---------|-----| -| Python | 3.12+ | Runtime | -| multiprocessing | stdlib | Paralelismo de procesos | -| FastAPI | 0.128.0 | API de salud (opcional) | -| azure-storage-queue | 12.14.1 | Consumir tareas de la cola | -| azure-cosmos | 4.14.3 | Leer pipelines y actualizar estados | -| azure-storage-blob | 12.27.1 | Acceso a documentos | -| contentflow-lib | local | Motor de pipelines | - -### 4.2 Arquitectura multi-proceso - -El Worker utiliza el módulo `multiprocessing` de Python para ejecutar múltiples procesos independientes: - -``` -┌──────────────────────────────────────────────────────────┐ -│ WORKER ENGINE │ -│ │ -│ ┌────────────────────────────┐ │ -│ │ Proceso Principal │ │ -│ │ (main.py) │ │ -│ │ │ │ -│ │ ├── Signal Handler │ │ -│ │ ├── WorkerEngine.run() │ │ -│ │ └── API Thread (8099) │ │ -│ └─────────────┬──────────────┘ │ -│ │ │ -│ ┌───────────┴───────────────────────────┐ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌────────────────────────┐ ┌────────────────────────┐ │ -│ │ Source Worker(s) │ │ Processing Worker(s) │ │ -│ │ (InputSourceWorker) │ │ (ContentProcessing │ │ -│ │ │ │ Worker) │ │ -│ │ Cantidad: N_SOURCE │ │ Cantidad: N_PROCESS │ │ -│ │ Default: 1 │ │ Default: 0 │ │ -│ │ │ │ │ │ -│ │ Ciclo: │ │ Ciclo: │ │ -│ │ 1. Leer vaults │ │ 1. Poll cola │ │ -│ │ 2. Ejecutar inputs │ │ 2. Recibir tarea │ │ -│ │ 3. Descubrir contenido│ │ 3. Leer pipeline │ │ -│ │ 4. Crear tareas │ │ 4. Ejecutar pipeline │ │ -│ │ 5. Dormir (300s) │ │ 5. Actualizar estado │ │ -│ └────────────────────────┘ └────────────────────────┘ │ -│ │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ multiprocessing.Event (stop_event) │ │ -│ │ → Señal compartida para apagado coordinado │ │ -│ └───────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────┘ -``` - -### 4.3 Worker de entrada (Input Source Worker) - -**Responsabilidad**: Descubrir contenido nuevo en los orígenes de datos configurados. - -**Flujo de ejecución**: - -``` -┌─ Bucle principal ────────────────────────────────┐ -│ │ -│ 1. Consultar Cosmos DB → vaults habilitados │ -│ 2. Para cada vault: │ -│ a. Leer pipeline asociado │ -│ b. Obtener checkpoint más reciente │ -│ c. Ejecutar ejecutor(es) de input │ -│ d. Descubrir documentos nuevos │ -│ e. Por cada documento nuevo: │ -│ - Crear ContentProcessingTask │ -│ - Encolar en Storage Queue │ -│ f. Guardar nuevo checkpoint │ -│ 3. Dormir (DEFAULT_POLLING_INTERVAL_SECONDS) │ -│ 4. Repetir hasta stop_event │ -│ │ -└───────────────────────────────────────────────────┘ -``` - -**Gestión de checkpoints**: El sistema implementa **rastreo incremental** para evitar reprocesar documentos: -- En la primera ejecución, no hay checkpoint (se descubren todos los documentos) -- En ejecuciones posteriores, solo se descubren documentos nuevos o modificados desde el último checkpoint -- Los checkpoints se almacenan en el contenedor `vault_crawl_checkpoints` de Cosmos DB - -### 4.4 Worker de procesamiento (Content Processing Worker) - -**Responsabilidad**: Ejecutar pipelines completos sobre elementos de contenido individuales. - -**Flujo de ejecución**: - -``` -┌─ Bucle principal ────────────────────────────────┐ -│ │ -│ 1. Poll Azure Storage Queue │ -│ (max 32 mensajes, visibilidad 300s) │ -│ 2. Para cada mensaje: │ -│ a. Deserializar ContentProcessingTask │ -│ b. Obtener bloqueo distribuido │ -│ c. Leer pipeline de Cosmos DB │ -│ d. Crear PipelineExecutor │ -│ e. Ejecutar pipeline (sin ejecutores input) │ -│ f. Actualizar estado en Cosmos DB │ -│ g. Eliminar mensaje de la cola │ -│ 3. Si no hay mensajes → dormir (5s) │ -│ 4. Repetir hasta stop_event │ -│ │ -└───────────────────────────────────────────────────┘ -``` - -### 4.5 Cliente de cola - -**TaskQueueClient** — Abstracción sobre Azure Storage Queue: - -``` -TaskQueueClient - │ - ├── ensure_queue_exists() - │ → Crea la cola si no existe - │ - ├── send_content_processing_task(task: ContentProcessingTask) - │ → Serializa tarea → JSON → Base64 → Envía a cola - │ - ├── send_input_source_task(task: InputSourceTask) - │ → Serializa tarea → JSON → Base64 → Envía a cola - │ - └── _send_message(message, visibility_timeout) - → Envío interno con manejo de errores -``` - -**Formato del mensaje en cola**: -```json -{ - "task_type": "CONTENT_PROCESSING", - "payload": { - "pipeline_id": "abc-123", - "vault_id": "def-456", - "content": { ... } - } -} -``` - -### 4.6 Ciclo de vida de una tarea - -``` - ┌──────────┐ - │ PENDING │ ← Tarea creada y encolada - └────┬─────┘ - │ - ┌────▼─────┐ - │ RUNNING │ ← Worker toma la tarea - └────┬─────┘ - │ - ┌────────┼────────┐ - │ │ │ - ┌────▼───┐ ┌──▼───┐ ┌─▼──────┐ - │COMPLETED│ │FAILED│ │CANCELLED│ - └────────┘ └──────┘ └────────┘ - │ │ - │ ┌────▼────┐ - │ │ RETRY? │ (max 3 reintentos) - │ └────┬────┘ - │ │ Sí - │ ┌────▼─────┐ - │ │ PENDING │ - │ └──────────┘ - │ - Resultado almacenado - en Cosmos DB + Blob -``` - -### 4.7 Configuración del Worker - -| Parámetro | Default | Descripción | -|-----------|---------|-------------| -| `WORKER_NAME` | `contentflow-worker` | Nombre del worker | -| `NUM_PROCESSING_WORKERS` | `0` | Cantidad de workers de procesamiento | -| `NUM_SOURCE_WORKERS` | `1` | Cantidad de workers de entrada | -| `QUEUE_POLL_INTERVAL_SECONDS` | `5` | Intervalo de consulta a la cola | -| `QUEUE_VISIBILITY_TIMEOUT_SECONDS` | `300` | Timeout de visibilidad (5 min) | -| `QUEUE_MAX_MESSAGES` | `32` | Máximo de mensajes por poll | -| `DEFAULT_POLLING_INTERVAL_SECONDS` | `300` | Intervalo de descubrimiento (5 min) | -| `LOCK_TTL_SECONDS` | `300` | Tiempo de vida del bloqueo distribuido | -| `MAX_TASK_RETRIES` | `3` | Reintentos máximos por tarea | -| `TASK_TIMEOUT_SECONDS` | `600` | Timeout por tarea (10 min) | -| `API_ENABLED` | `true` | Habilitar API de salud | -| `API_PORT` | `8099` | Puerto del API de salud | - -### 4.8 Diagrama del motor de workers - -``` -┌─────────────────────────────────────────────────────────────┐ -│ WORKER ENGINE │ -│ │ -│ Señales del SO ──▶ Signal Handler ──▶ stop_event.set() │ -│ (SIGINT/SIGTERM) │ -│ │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ WorkerEngine.run() │ │ -│ │ ├── start() │ │ -│ │ │ ├── Crear N source workers (mp.Process) │ │ -│ │ │ └── Crear N processing workers (mp.Process) │ │ -│ │ │ │ │ -│ │ └── monitor loop (mientras no stop_event): │ │ -│ │ ├── Verificar procesos activos │ │ -│ │ ├── Reemplazar workers caídos │ │ -│ │ └── sleep(1 segundo) │ │ -│ └────────────────────────────────────────────────────┘ │ -│ │ -│ Apagado: │ -│ 1. stop_event.set() │ -│ 2. join(timeout=30s) por cada worker │ -│ 3. terminate() workers que no respondieron │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 5. Componente: Librería Core (contentflow-lib) - -### 5.1 Motor de pipelines - -El motor de pipelines es el cerebro de ContentFlow. Convierte definiciones YAML declarativas en grafos de ejecución: - -``` -YAML Pipeline Definition - │ - ▼ -┌─────────────────┐ -│ PipelineFactory │ ← Parsea YAML y crea instancias de ejecutores -│ │ -│ parse_yaml() │ -│ create_pipeline()│ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ DAG (Grafo │ ← Grafo dirigido acíclico de ejecutores -│ Dirigido │ -│ Acíclico) │ -│ │ -│ Nodos: Ejecutors│ -│ Aristas: Depend.│ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│PipelineExecutor │ ← Ejecuta el DAG resolviendo dependencias -│ │ -│ execute() │ Características: -│ │ • Ejecución asíncrona (asyncio) -│ │ • Resolución de dependencias -│ │ • Evaluación de condiciones -│ │ • Manejo de errores por ejecutor -│ │ • Eventos de ejecución -│ │ • Estadísticas de rendimiento -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ PipelineResult │ ← Resultado final de la ejecución -│ │ -│ status: Success │ -│ outputs: {...} │ -│ events: [...] │ -│ duration: 45.2s │ -└─────────────────┘ -``` - -### 5.2 Jerarquía de clases de ejecutores - -``` -Executor (Agent Framework) - │ - └── BaseExecutor (ABC) ─── contentflow/executors/base.py - │ - │ Propiedades comunes: - │ ├── id: str - │ ├── settings: Dict - │ ├── enabled: bool - │ ├── condition: str (evaluación condicional) - │ ├── fail_pipeline_on_error: bool - │ └── debug_mode: bool - │ - │ Métodos: - │ ├── get_setting(key, default) - │ ├── resolve_env_vars(value) - │ ├── _evaluate_condition(content) - │ └── process_input(input, ctx) → Content | List[Content] [ABSTRACTO] - │ - ├── InputExecutor (ABC) ─── contentflow/executors/input_executor.py - │ │ - │ │ Propiedades adicionales: - │ │ ├── polling_interval_seconds: int (300) - │ │ ├── max_results: int (1000) - │ │ └── batch_size: int (100) - │ │ - │ │ Métodos: - │ │ ├── crawl(checkpoint) → (List[Content], token) [ABSTRACTO] - │ │ └── crawl_all(checkpoint) → List[Content] - │ │ - │ └── Implementaciones: - │ └── AzureBlobInputDiscoveryExecutor - │ - └── Ejecutores concretos (35+): - ├── Extracción - ├── Procesamiento IA - ├── Transformación - ├── Salida - ├── Enrutamiento - └── Especializados -``` - -### 5.3 Catálogo completo de ejecutores - -#### Entrada y recuperación de contenido - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `azure_blob_input_discovery` | Azure Blob Input Discovery | Descubre archivos en contenedores Blob Storage | -| `azure_blob_content_retriever` | Azure Blob Content Retriever | Recupera el contenido de blobs descubiertos | -| `content_retriever` | Content Retriever | Recuperador genérico de contenido | - -#### Extracción de documentos - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `pdf_extractor` | PDF Extractor | Extrae texto de documentos PDF | -| `word_extractor` | Word Extractor | Extrae texto de documentos Word (.docx) | -| `excel_extractor` | Excel Extractor | Extrae datos de hojas de cálculo Excel | -| `powerpoint_extractor` | PowerPoint Extractor | Extrae texto de presentaciones PowerPoint | -| `csv_extractor` | CSV Extractor | Extrae datos de archivos CSV | -| `azure_document_intelligence_extractor` | Azure Document Intelligence | Extracción inteligente con OCR y modelos pre-entrenados | -| `azure_content_understanding_extractor` | Azure Content Understanding | Análisis avanzado de contenido con AI | - -#### Procesamiento con IA - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `azure_openai_agent` | Azure OpenAI Agent | Procesamiento con modelos GPT (análisis, generación) | -| `azure_openai_embeddings` | Azure OpenAI Embeddings | Genera vectores de embedding para búsqueda semántica | -| `text_summarizer` | Text Summarizer | Genera resúmenes de texto usando IA | -| `entity_extractor` | Entity Extractor | Extrae entidades nombradas (personas, organizaciones, lugares) | -| `sentiment_analyser` | Sentiment Analyser | Análisis de sentimiento (positivo, negativo, neutro) | -| `content_classifier` | Content Classifier | Clasifica contenido en categorías definidas | -| `pii_detector` | PII Detector | Detecta información personal identificable | -| `keyword_extractor` | Keyword Extractor | Extrae palabras clave y frases importantes | -| `language_detector` | Language Detector | Detecta el idioma del contenido | -| `content_translator` | Content Translator | Traduce contenido entre idiomas | - -#### Transformación de datos - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `recursive_text_chunker` | Recursive Text Chunker | Divide texto en fragmentos con solapamiento configurable | -| `table_row_splitter` | Table Row Splitter | Divide tablas en filas individuales para procesamiento | -| `field_mapper` | Field Mapper | Mapea y transforma campos entre el modelo de contenido | -| `field_selector` | Field Selector | Selecciona campos específicos del contenido | -| `fan_in_aggregator` | Fan-In Aggregator | Agrega resultados de procesamiento paralelo | - -#### Salida y almacenamiento - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `azure_blob_output` | Azure Blob Output | Escribe contenido procesado a Blob Storage | -| `ai_search_index_output` | AI Search Index Output | Indexa contenido en Azure AI Search | -| `gptrag_search_index_document_generator` | GPT RAG Index Generator | Genera documentos para índices RAG | - -#### Enrutamiento y orquestación - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `subpipeline` | Sub-Pipeline | Ejecuta un sub-pipeline anidado | -| `for_each_content` | For Each Content | Itera sobre una lista de contenidos | -| `pass_through` | Pass Through | Pasa contenido sin modificar (debug/testing) | - -#### Conjuntos de documentos - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `document_set_initializer` | Document Set Initializer | Inicializa un conjunto de documentos para procesamiento cruzado | -| `document_set_collector` | Document Set Collector | Recopila documentos en un conjunto | -| `cross_document_comparison` | Cross-Document Comparison | Compara contenido entre múltiples documentos | -| `cross_document_field_aggregator` | Cross-Document Field Aggregator | Agrega campos de múltiples documentos | - -#### Especializados - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `web_scraper` | Web Scraper | Extrae contenido de páginas web (Playwright) | -| `cosmos_db_lookup` | Cosmos DB Lookup | Consulta datos en Azure Cosmos DB | - -### 5.4 Modelo de contenido (Content) - -El modelo `Content` es la estructura de datos central que fluye por los pipelines: - -``` -Content -│ -├── Identificación -│ ├── id: str (UUID único) -│ └── source: str (origen: blob://container/path/file.pdf) -│ -├── Datos extraídos -│ ├── content: str (texto completo extraído) -│ ├── content_type: str (MIME type) -│ └── content_hash: str (hash para detección de cambios) -│ -├── Metadatos -│ ├── metadata: Dict[str, Any] -│ │ ├── filename, size, last_modified -│ │ ├── page_count, language -│ │ └── custom fields... -│ │ -│ └── tags: List[str] -│ -├── Fragmentos y embeddings -│ ├── chunks: List[Dict] (fragmentos de texto) -│ └── embeddings: List[float] (vectores numéricos) -│ -├── Resultados de análisis -│ ├── analysis: Dict[str, Any] -│ │ ├── entities, sentiment, keywords -│ │ ├── summary, classification -│ │ └── pii_detected, language -│ │ -│ └── executor_outputs: Dict[str, Any] -│ └── {executor_id: output_data} -│ -└── Registro de ejecución - └── log_entries: List[ExecutorLogEntry] - └── {executor_id, timestamp, status, duration_ms} -``` - -### 5.5 Conectores de Azure - -``` -contentflow/connectors/ -│ -├── Azure Blob Storage Connector -│ ├── Listar blobs en contenedores -│ ├── Descargar contenido de blobs -│ ├── Subir contenido a blobs -│ └── Gestionar metadatos de blobs -│ -├── Azure AI Search Connector -│ ├── Crear/actualizar índices -│ ├── Indexar documentos -│ └── Búsqueda híbrida (texto + vectores) -│ -├── Azure Document Intelligence Connector -│ ├── Analizar documentos (OCR) -│ ├── Modelos pre-entrenados (facturas, recibos, etc.) -│ └── Modelos personalizados -│ -├── Azure OpenAI Connector -│ ├── Completions (GPT-4.1, GPT-4.1-mini) -│ ├── Embeddings (text-embedding-3-small) -│ └── Chat completions -│ -└── Azure Cosmos DB Connector - ├── Consultas SQL NoSQL - ├── CRUD de documentos - └── Bulk operations -``` - -### 5.6 Diagrama del motor de pipelines - -``` -Definición YAML del Pipeline -┌──────────────────────────────────────────────────┐ -│ name: procesar-documentos │ -│ steps: │ -│ - executor: blob_input ─────────┐ │ -│ - executor: pdf_extractor ──────┐ │ │ -│ - executor: chunker ─────────┐ │ │ │ -│ - executor: embeddings ────┐ │ │ │ │ -│ - executor: blob_output ─┐ │ │ │ │ │ -└────────────────────────────┼─┼─┼──┼──┼────────────┘ - │ │ │ │ │ - PipelineFactory.parse() - │ - ▼ - ┌─ DAG de ejecución ────────────┐ - │ │ - │ [blob_input] │ - │ │ │ - │ ▼ │ - │ [pdf_extractor] │ - │ │ │ - │ ▼ │ - │ [chunker] │ - │ │ │ - │ ▼ │ - │ [embeddings] │ - │ │ │ - │ ▼ │ - │ [blob_output] │ - │ │ - └────────────────────────────────┘ - │ - PipelineExecutor.execute() - │ - ▼ - ┌─ Ejecución paso a paso ───────┐ - │ │ - │ Content ──▶ blob_input │ - │ Content ──▶ pdf_extractor │ - │ Content ──▶ chunker │ - │ Content ──▶ embeddings │ - │ Content ──▶ blob_output │ - │ │ - │ Cada paso: │ - │ 1. Evaluar condición │ - │ 2. Si enabled: ejecutar │ - │ 3. Capturar resultado/error │ - │ 4. Emitir evento │ - │ 5. Pasar al siguiente │ - │ │ - └────────────────────────────────┘ - │ - ▼ - PipelineResult - (status, outputs, - events, duration) -``` - ---- - -## 6. Infraestructura de Azure (infra) - -### 6.1 Recursos desplegados - -ContentFlow despliega los siguientes recursos de Azure: - -``` -Resource Group -│ -├── User-Assigned Managed Identity (id-${token}) -│ -├── Log Analytics Workspace (log-${token}) -│ └── Recolección de logs de todos los servicios -│ -├── Application Insights (appi-${token}) -│ └── Monitoreo, métricas y trazas distribuidas -│ -├── Cosmos DB Account (cosmos-${token}) -│ └── Database: contentflow -│ ├── executor_catalog -│ ├── pipelines -│ ├── vaults -│ ├── vault_executions -│ ├── vault_exec_locks -│ ├── vault_crawl_checkpoints -│ └── pipeline_executions -│ -├── Storage Account (st${token}) -│ ├── Blob Container: content -│ └── Queue: contentflow-execution-requests -│ -├── Container Registry (cr${token}) -│ └── Imágenes Docker de los 3 servicios -│ -├── App Configuration Store (appcs-${token}) -│ └── 25+ claves de configuración -│ -├── Container Apps Environment (cae-${token}) -│ ├── Container App: api-${token} (Puerto 8090) -│ ├── Container App: worker-${token} (Puerto 8099) -│ └── Container App: web-${token} -│ -└── AI Foundry (opcional) - ├── AI Services (S0) - └── Deployments: - ├── gpt-4.1-mini (GlobalStandard, Cap: 100) - └── gpt-4.1 (GlobalStandard, Cap: 100) -``` - -### 6.2 SKUs y niveles de servicio - -| Recurso | SKU / Nivel | Detalles | -|---------|------------|----------| -| **Cosmos DB** | Serverless | Pago por consumo, sin capacidad reservada | -| **Storage Account** | Standard_LRS | Locally Redundant Storage, Hot tier | -| **Container Registry** | Standard | Premium si se usan endpoints privados | -| **App Configuration** | Standard | Configuración centralizada | -| **Log Analytics** | PerGB2018 | Pago por GB ingerido | -| **Application Insights** | Workspace-based | Conectado a Log Analytics | -| **AI Foundry / AI Services** | S0 | Tier estándar de servicios cognitivos | -| **GPT-4.1** | GlobalStandard | Capacidad: 100 TPM | -| **GPT-4.1-mini** | GlobalStandard | Capacidad: 100 TPM | -| **Container Apps** | Consumption | Serverless, perfil de carga de trabajo por consumo | -| **Managed Identity** | User-Assigned | N/A (sin SKU, es un recurso de identidad) | - -### 6.3 Módulos Bicep - -| Módulo | Archivo | Azure Verified Module (AVM) | Versión AVM | -|--------|---------|----------------------------|-------------| -| Identidad administrada | `user-assigned-identity.bicep` | `avm/res/managed-identity/user-assigned-identity` | 0.4.1 | -| Log Analytics | `log-analytics-ws.bicep` | Built-in | — | -| Application Insights | `app-insights.bicep` | Built-in | — | -| Cosmos DB | `cosmos.bicep` | `avm/res/document-db/database-account` | 0.16.0 | -| Storage Account | `storage.bicep` | `avm/res/storage/storage-account` | 0.27.1 | -| Container Registry | `container-registry.bicep` | `avm/res/container-registry` | Built-in | -| Container Apps Environment | `container-apps-environment.bicep` | `avm/res/app/managed-environment` | 0.11.3 | -| Container App | `container-app.bicep` | `avm/res/app/container-app` | 0.19.0 | -| App Configuration | `app-config-store.bicep` | Built-in | — | -| AI Foundry | `ai-foundry.bicep` | `avm/ptn/ai-ml/ai-foundry` | 0.5.0 | -| Static Web App | `static-web-app.bicep` | Built-in (no activo) | — | - -### 6.4 Cosmos DB — Estructura de base de datos - -``` -Cosmos DB Account (cosmos-${token}) -│ -├── Tipo: NoSQL (SQL API) -├── Consistencia: Session -├── Replicación: Región única (automaticFailover: false) -├── Backup: Continuous7Days (hasta 30 días de retención) -├── Autenticación: Solo Managed Identity (contraseñas deshabilitadas) -│ -└── Database: "contentflow" - │ - ├── executor_catalog (/id) - │ └── Definiciones de ejecutores del catálogo - │ - ├── pipelines (/id) - │ └── Configuración y YAML de cada pipeline - │ - ├── vaults (/id) - │ └── Definiciones de bóvedas de documentos - │ - ├── vault_executions (/id) - │ └── Registro de ejecuciones por vault - │ - ├── vault_exec_locks (/id) - │ └── Bloqueos distribuidos para evitar ejecuciones duplicadas - │ - ├── vault_crawl_checkpoints (/id) - │ └── Puntos de control para rastreo incremental - │ - └── pipeline_executions (/id) - └── Historial detallado de ejecuciones de pipelines -``` - -### 6.5 Storage Account — Blobs y colas - -``` -Storage Account (st${token}) -│ -├── Tipo: StorageV2 -├── SKU: Standard_LRS -├── Acceso: Hot tier -├── Autenticación: Solo Managed Identity (sharedKeyAccess: false) -├── Retención de eliminados: 7 días (blobs y contenedores) -│ -├── Blob Containers: -│ └── "content" (acceso público: None) -│ ├── Documentos originales -│ └── Contenido procesado -│ -└── Queues: - └── "contentflow-execution-requests" - └── Mensajes de tareas (InputSource y ContentProcessing) -``` - -### 6.6 Container Apps — Configuración de servicios - -| Configuración | API | Worker | Web | -|---------------|-----|--------|-----| -| **Puerto** | 8090 | 8099 | HTTP estándar | -| **CPU** | 1 core | 1 core | 1 core | -| **Memoria** | 2 GiB | 2 GiB | 2 GiB | -| **Réplicas mínimas** | 1 | 1 | 1 | -| **Réplicas máximas** | 2 | 2 | 2 | -| **Ingress** | Externo | Externo | Externo | -| **Escalado** | HTTP (10 concurrentes) | HTTP (10 concurrentes) | HTTP (10 concurrentes) | -| **Health check** | `/health` (60s intervalo) | — | — | -| **CORS** | GET, POST, PUT, DELETE, OPTIONS, PATCH | GET, POST, PUT, DELETE, OPTIONS, PATCH | GET, POST, PUT, DELETE, OPTIONS, PATCH | -| **Perfil** | Consumption | Consumption | Consumption | - -### 6.7 AI Foundry — Modelos de IA - -``` -AI Foundry -│ -├── AI Services Account (S0) -│ └── Nombre del proyecto: "contentflow-project" -│ -└── Model Deployments: - │ - ├── gpt-4.1-mini - │ ├── Formato: OpenAI - │ ├── Versión: 2025-04-14 - │ ├── SKU: GlobalStandard - │ └── Capacidad: 100 (TPM) - │ - └── gpt-4.1 - ├── Formato: OpenAI - ├── Versión: 2025-04-14 - ├── SKU: GlobalStandard - └── Capacidad: 100 (TPM) -``` - -### 6.8 Diagrama de infraestructura Azure - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ RESOURCE GROUP │ -│ │ -│ ┌─ Identidad ─────────────┐ ┌─ Monitoreo ────────────────────┐ │ -│ │ │ │ │ │ -│ │ Managed Identity │ │ Log Analytics App Insights │ │ -│ │ (User-Assigned) │ │ (PerGB2018) (Workspace) │ │ -│ │ │ │ │ │ -│ │ RBAC Roles: │ └─────────────────────────────────┘ │ -│ │ • Blob Data Contributor │ │ -│ │ • Queue Data Contributor│ ┌─ Configuración ────────────────┐ │ -│ │ • Cosmos DB Data Contrib│ │ │ │ -│ └──────────────────────────┘ │ App Configuration (Standard) │ │ -│ │ 25+ claves de configuración │ │ -│ ┌─ Cómputo ───────────────────┐│ │ │ -│ │ ││ Claves: │ │ -│ │ Container Apps Environment ││ • COSMOS_DB_ENDPOINT │ │ -│ │ (Consumption workload) ││ • BLOB_STORAGE_ACCOUNT_NAME │ │ -│ │ ││ • STORAGE_WORKER_QUEUE_URL │ │ -│ │ ┌──────┐ ┌──────┐ ┌──────┐ ││ • ... │ │ -│ │ │ API │ │Worker│ │ Web │ │└─────────────────────────────────┘ │ -│ │ │ 8090 │ │ 8099 │ │ 80 │ │ │ -│ │ │ 1CPU │ │ 1CPU │ │ 1CPU │ │ ┌─ IA ────────────────────────┐ │ -│ │ │ 2GiB │ │ 2GiB │ │ 2GiB │ │ │ │ │ -│ │ └──────┘ └──────┘ └──────┘ │ │ AI Foundry (S0) │ │ -│ │ │ │ ├── gpt-4.1-mini (100 TPM)│ │ -│ │ Container Registry (Std) │ │ └── gpt-4.1 (100 TPM) │ │ -│ │ (Imágenes Docker) │ │ │ │ -│ └──────────────────────────────┘ └─────────────────────────────┘ │ -│ │ -│ ┌─ Datos ──────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ Cosmos DB (Serverless) Storage Account (Standard_LRS) │ │ -│ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ -│ │ │ DB: contentflow │ │ Blob: "content" │ │ │ -│ │ │ ├── executor_catalog│ │ Queue: "contentflow- │ │ │ -│ │ │ ├── pipelines │ │ execution-requests" │ │ │ -│ │ │ ├── vaults │ └────────────────────────────┘ │ │ -│ │ │ ├── vault_executions│ │ │ -│ │ │ ├── vault_exec_locks│ │ │ -│ │ │ ├── vault_crawl_ │ │ │ -│ │ │ │ checkpoints │ │ │ -│ │ │ └── pipeline_ │ │ │ -│ │ │ executions │ │ │ -│ │ └─────────────────────┘ │ │ -│ └───────────────────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 7. Interconexión entre componentes - -### 7.1 Flujo de datos completo - -``` -┌─────────┐ HTTP ┌─────────┐ Write ┌──────────┐ Poll ┌──────────┐ -│ WEB │────────▶│ API │────────▶│ QUEUE │────────▶│ WORKER │ -│ (React) │ REST │(FastAPI)│ Queue │ (Azure │ Recv │ (Python) │ -└─────────┘ └────┬────┘ │ Storage) │ └────┬─────┘ - │ └──────────┘ │ - │ │ - ┌────▼────┐ ┌────▼─────┐ - │COSMOS DB│◀─────────────────────────────│ PIPELINE │ - │(Metadat)│ Lee pipelines, actualiza │ ENGINE │ - └─────────┘ estados de ejecución │ (Lib) │ - │ └────┬─────┘ - ┌────▼────┐ │ - │ BLOB │◀───────────────────────────── │ - │STORAGE │ Lee documentos, guarda │ - │(Content)│ resultados procesados │ - └─────────┘ │ - ┌────▼─────┐ - │AZURE AI │ - │SERVICES │ - │(GPT, Doc │ - │ Intell.) │ - └──────────┘ -``` - -### 7.2 Flujo de mensajes por cola - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ FLUJO DE MENSAJES POR COLA │ -│ │ -│ ① API recibe solicitud de ejecución │ -│ POST /api/pipelines/{id}/execute │ -│ │ │ -│ ▼ │ -│ ② API crea registro de ejecución en Cosmos DB │ -│ PipelineExecution { status: "pending" } │ -│ │ │ -│ ▼ │ -│ ③ API envía InputSourceTask a la cola │ -│ ┌───────────────────────────────────────────┐ │ -│ │ Queue: contentflow-execution-requests │ │ -│ │ Message: { task_type: "INPUT_SOURCE", │ │ -│ │ payload: { pipeline_id, ... } } │ │ -│ └────────────────────┬──────────────────────┘ │ -│ │ │ -│ ④ Source Worker consume la tarea │ -│ │ │ -│ ▼ │ -│ ⑤ Source Worker ejecuta ejecutores de input │ -│ → Descubre N documentos │ -│ │ │ -│ ▼ │ -│ ⑥ Source Worker crea N tareas ContentProcessingTask │ -│ ┌───────────────────────────────────────────┐ │ -│ │ Queue: contentflow-execution-requests │ │ -│ │ Message × N: │ │ -│ │ { task_type: "CONTENT_PROCESSING", │ │ -│ │ payload: { pipeline_id, content, ... } } │ │ -│ └────────────────────┬──────────────────────┘ │ -│ │ │ -│ ⑦ Processing Workers consumen tareas (en paralelo) │ -│ │ │ -│ ▼ │ -│ ⑧ Cada worker ejecuta el pipeline completo sobre un documento │ -│ (excluyendo ejecutores de input) │ -│ │ │ -│ ▼ │ -│ ⑨ Resultados almacenados en Blob Storage / Cosmos DB │ -│ PipelineExecution { status: "completed" } │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 7.3 Flujo de autenticación - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ CADENA DE AUTENTICACIÓN │ -│ │ -│ User-Assigned Managed Identity │ -│ (id-${resourceToken}) │ -│ │ │ -│ ├── DefaultAzureCredential │ -│ │ (Cadena de credenciales automática) │ -│ │ │ -│ ├──▶ Cosmos DB ──── rol: "Cosmos DB SQL Data Contributor" │ -│ │ (Lectura/escritura de documentos) │ -│ │ │ -│ ├──▶ Blob Storage ─ rol: "Storage Blob Data Contributor" │ -│ │ (Lectura/escritura de blobs) │ -│ │ │ -│ ├──▶ Storage Queue ─ rol: "Storage Queue Data Contributor"│ -│ │ (Envío/recepción de mensajes) │ -│ │ │ -│ └──▶ App Config ──── rol: "App Configuration Data Reader" │ -│ (Lectura de configuración) │ -│ │ -│ Nota: Contraseñas y claves compartidas están DESHABILITADAS │ -│ (disableLocalAuthentication: true, allowSharedKeyAccess: false) │ -└──────────────────────────────────────────────────────────────────┘ -``` - -### 7.4 Matriz de comunicación entre servicios - -| Origen | Destino | Protocolo | Propósito | -|--------|---------|-----------|-----------| -| **Web** → API | HTTP/REST | CRUD de pipelines, vaults, catálogo | -| **API** → Cosmos DB | SDK Azure (HTTPS) | Almacenar/leer metadatos | -| **API** → Blob Storage | SDK Azure (HTTPS) | Gestionar documentos | -| **API** → Storage Queue | SDK Azure (HTTPS) | Encolar tareas | -| **API** → App Configuration | SDK Azure (HTTPS) | Leer configuración | -| **Worker** → Storage Queue | SDK Azure (HTTPS) | Consumir tareas | -| **Worker** → Cosmos DB | SDK Azure (HTTPS) | Leer pipelines, actualizar estados | -| **Worker** → Blob Storage | SDK Azure (HTTPS) | Leer/escribir contenido | -| **Worker** → Azure AI Services | SDK Azure (HTTPS) | Procesamiento IA | -| **Worker** → App Configuration | SDK Azure (HTTPS) | Leer configuración | -| **Todos** → App Insights | SDK Azure (HTTPS) | Telemetría y trazas | - -### 7.5 Diagrama de secuencia: Ejecución de pipeline - -``` - Usuario Web (React) API (FastAPI) Cosmos DB Storage Queue Source Worker Processing Worker Azure AI - │ │ │ │ │ │ │ │ - │ ① Crear │ │ │ │ │ │ │ - │ pipeline │ │ │ │ │ │ │ - │───────────────▶│ │ │ │ │ │ │ - │ │ POST /pipeline │ │ │ │ │ │ - │ │───────────────▶│ │ │ │ │ │ - │ │ │──── save ────▶│ │ │ │ │ - │ │ │◀──── ok ──────│ │ │ │ │ - │ │◀───── 201 ─────│ │ │ │ │ │ - │◀───────────────│ │ │ │ │ │ │ - │ │ │ │ │ │ │ │ - │ ② Ejecutar │ │ │ │ │ │ │ - │───────────────▶│ │ │ │ │ │ │ - │ │ POST /execute │ │ │ │ │ │ - │ │───────────────▶│ │ │ │ │ │ - │ │ │── save exec ─▶│ │ │ │ │ - │ │ │── queue task ─┼─────────────▶│ │ │ │ - │ │◀─── 202 ──────│ │ │ │ │ │ - │◀───────────────│ │ │ │ │ │ │ - │ │ │ │ │ │ │ │ - │ │ │ │ │ ③ poll │ │ │ - │ │ │ │ │◀─────────────│ │ │ - │ │ │ │ │── InputTask──▶ │ │ - │ │ │ │◀─── read pipeline ──────────│ │ │ - │ │ │ │──── pipeline data ─────────▶│ │ │ - │ │ │ │ │ │ │ │ - │ │ │ │ │ ④ descubrir │ │ │ - │ │ │ │ │ N documentos│ │ │ - │ │ │ │ │◀── N tasks ──│ │ │ - │ │ │ │ │ │ │ │ - │ │ │ │ │ ⑤ poll │ │ │ - │ │ │ │ │◀─────────────┼───────────────│ │ - │ │ │ │ │─ ContentTask─┼──────────────▶│ │ - │ │ │ │◀────────── read pipeline ───┼───────────────│ │ - │ │ │ │ │ │ │ - │ │ │ │ │ │ ⑥ ejecutar │ │ - │ │ │ │ │ │ pipeline │ │ - │ │ │ │ │ │──────────────▶│──── AI ─────▶│ - │ │ │ │ │ │ │◀─── result ──│ - │ │ │ │◀──────────── update status ─┼───────────────│ │ - │ │ │ │ │ │ │ │ - │ ⑦ Consultar │ │ │ │ │ │ │ - │───────────────▶│ GET /executions│ │ │ │ │ │ - │ │───────────────▶│── query ─────▶│ │ │ │ │ - │ │ │◀── results ───│ │ │ │ │ - │ │◀──── 200 ──────│ │ │ │ │ │ - │◀───────────────│ │ │ │ │ │ │ -``` - ---- - -## 8. Modos de despliegue - -### 8.1 Modo básico - -``` -┌─────────────────────────────────────────────────────────────┐ -│ MODO BÁSICO │ -│ │ -│ Ideal para: Desarrollo, pruebas, demos │ -│ │ -│ Características: │ -│ ✓ Endpoints públicos (accesibles desde internet) │ -│ ✓ Todos los recursos creados nuevos │ -│ ✓ Red virtual creada automáticamente │ -│ ✓ Log Analytics + App Insights propios │ -│ ✗ Sin endpoints privados │ -│ ✗ Sin integración con redes existentes │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ INTERNET │ │ -│ │ │ │ │ -│ │ ┌──────────────────┼───────────────────┐ │ │ -│ │ │ Container Apps Environment │ │ │ -│ │ │ │ │ │ │ -│ │ │ ┌──────┐ ┌───┴───┐ ┌──────┐ │ │ │ -│ │ │ │ Web │ │ API │ │Worker│ │ │ │ -│ │ │ │(pub) │ │(pub) │ │(pub) │ │ │ │ -│ │ │ └──────┘ └───────┘ └──────┘ │ │ │ -│ │ └──────────────────────────────────────┘ │ │ -│ │ │ │ │ -│ │ ┌──────────────────┼───────────────────┐ │ │ -│ │ │ Recursos con acceso público │ │ │ -│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ -│ │ │ │CosmosDB│ │Storage │ │AppConf │ │ │ │ -│ │ │ │(public)│ │(public)│ │(public) │ │ │ │ -│ │ │ └────────┘ └────────┘ └────────┘ │ │ │ -│ │ └──────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 8.2 Modo AILZ (AI Landing Zone) - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ MODO AILZ (AI Landing Zone) │ -│ │ -│ Ideal para: Producción empresarial │ -│ │ -│ Características: │ -│ ✓ Endpoints privados para todos los datos │ -│ ✓ Integración con VNet existente │ -│ ✓ Zonas DNS privadas │ -│ ✓ Shared Log Analytics + App Insights (del Landing Zone) │ -│ ✓ Container Registry Premium (endpoints privados) │ -│ ✓ Cumplimiento de seguridad empresarial │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ AI Landing Zone VNet (Existente) │ │ -│ │ │ │ -│ │ ┌─ aca-env-subnet ────────────────────────────────────┐ │ │ -│ │ │ │ │ │ -│ │ │ Container Apps Environment (internal VNet) │ │ │ -│ │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ -│ │ │ │ Web │ │ API │ │Worker│ │ │ │ -│ │ │ └──────┘ └──────┘ └──────┘ │ │ │ -│ │ │ │ │ │ -│ │ └──────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌─ pe-subnet (Private Endpoints) ─────────────────────┐ │ │ -│ │ │ │ │ │ -│ │ │ ┌────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ │ │ -│ │ │ │CosmosDB│ │Blob PE │ │Queue PE │ │ACR PE │ │ │ │ -│ │ │ │ PE │ │ │ │ │ │ │ │ │ │ -│ │ │ └────────┘ └────────┘ └──────────┘ └────────┘ │ │ │ -│ │ │ │ │ │ -│ │ └──────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ Zonas DNS Privadas: │ │ -│ │ • *.documents.azure.com (Cosmos DB) │ │ -│ │ • *.blob.core.windows.net (Blob Storage) │ │ -│ │ • *.queue.core.windows.net (Storage Queue) │ │ -│ │ • *.azurecr.io (Container Registry) │ │ -│ │ • *.azconfig.io (App Configuration) │ │ -│ │ • *.cognitiveservices.azure.com (AI Services) │ │ -│ │ • Container Apps custom domain │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 8.3 Proceso de despliegue con azd - -``` -$ azd up -│ -├── ① pre-provision.sh -│ ├── Validar Azure CLI instalado -│ ├── Validar azd instalado -│ └── Validar Docker instalado -│ -├── ② Provisionar infraestructura (Bicep) -│ ├── Crear Managed Identity -│ ├── Crear Log Analytics + App Insights (básico) -│ ├── Crear Cosmos DB + containers -│ ├── Crear Storage Account + blob + queue -│ ├── Crear Container Registry -│ ├── Crear App Configuration + claves -│ ├── Crear Container Apps Environment -│ ├── Crear Container Apps (API, Worker, Web) -│ ├── Crear AI Foundry + deployments (opcional) -│ └── Asignar roles RBAC -│ -├── ③ Build y Push de imágenes Docker -│ ├── docker build contentflow-api/ -│ ├── docker build contentflow-worker/ -│ ├── docker build contentflow-web/ -│ └── docker push → Container Registry -│ -├── ④ Desplegar servicios en Container Apps -│ ├── Actualizar imagen del API -│ ├── Actualizar imagen del Worker -│ └── Actualizar imagen del Web -│ -├── ⑤ post-deploy-api.sh -│ └── Configuración post-despliegue del API -│ -└── ⑥ post-deploy.sh - └── Tareas finales interactivas - -Resultado: URLs de los servicios desplegados - → API: https://api-xxxx.azurecontainerapps.io - → Worker: https://worker-xxxx.azurecontainerapps.io - → Web: https://web-xxxx.azurecontainerapps.io -``` - -### 8.4 Diagrama comparativo de modos - -``` - MODO BÁSICO MODO AILZ - ┌─────────────────────┐ ┌─────────────────────────┐ - │ │ │ │ - │ ☁️ Internet │ │ ☁️ Internet (limitado) │ - │ │ │ │ │ │ - │ ┌────▼────┐ │ │ ┌────▼────┐ │ - │ │Container│ │ │ │Container│ │ - │ │Apps │ │ │ │Apps │ │ - │ │(público)│ │ │ │(VNet │ │ - │ └────┬────┘ │ │ │internal)│ │ - │ │ │ │ └────┬────┘ │ - │ ┌────▼───┐ │ │ ┌────▼────┐ │ - │ │Recursos│ │ │ │ Private │ │ - │ │público │ │ │ │Endpoints│ │ - │ │acceso │ │ │ │(VNet) │ │ - │ └────────┘ │ │ └─────────┘ │ - │ │ │ │ - │ ✅ Rápido │ │ ✅ Seguro │ - │ ✅ Simple │ │ ✅ Empresarial │ - │ ❌ No para prod. │ │ ❌ Requiere VNet/LZ │ - └─────────────────────┘ └─────────────────────────┘ -``` - ---- - -## 9. Seguridad y autenticación - -### 9.1 Modelo de identidad - -ContentFlow utiliza **Azure Managed Identity** (identidad administrada asignada por el usuario) para toda la autenticación entre servicios. No se utilizan contraseñas, claves de acceso ni cadenas de conexión. - -``` -┌──────────────────────────────────────────────────────┐ -│ USER-ASSIGNED MANAGED IDENTITY │ -│ (id-${resourceToken}) │ -│ │ -│ Utilizada por: │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ API │ │ Worker │ │ Web │ │ -│ │ Service │ │ Service │ │ (build) │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ │ -│ Autenticación en código: │ -│ DefaultAzureCredential() → cadena automática: │ -│ 1. Environment variables │ -│ 2. Managed Identity │ -│ 3. Azure CLI (desarrollo local) │ -│ 4. Azure PowerShell │ -└──────────────────────────────────────────────────────┘ -``` - -### 9.2 Roles RBAC asignados - -| Recurso | Rol RBAC | Asignado a | Permiso | -|---------|----------|-----------|---------| -| **Cosmos DB** | Cosmos DB SQL Data Contributor | Managed Identity | Leer/escribir datos | -| **Cosmos DB** | Cosmos DB SQL Data Contributor | Deployer (principal) | Leer/escribir datos | -| **Blob Storage** | Storage Blob Data Contributor | Managed Identity | Leer/escribir blobs | -| **Blob Storage** | Storage Blob Data Contributor | Deployer (principal) | Leer/escribir blobs | -| **Storage Queue** | Storage Queue Data Contributor | Managed Identity | Enviar/recibir mensajes | -| **Storage Queue** | Storage Queue Data Contributor | Deployer (principal) | Enviar/recibir mensajes | -| **App Configuration** | App Configuration Data Reader | Managed Identity | Leer configuración | -| **Container Registry** | AcrPull | Managed Identity | Descargar imágenes | - -### 9.3 Diagrama de seguridad - -``` -┌────────────────────────────────────────────────────────────────────┐ -│ MODELO DE SEGURIDAD │ -│ │ -│ ┌─ Autenticación ─────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ✅ Managed Identity (sin contraseñas) │ │ -│ │ ✅ DefaultAzureCredential (cadena automática) │ │ -│ │ ✅ disableLocalAuthentication: true (Cosmos DB) │ │ -│ │ ✅ allowSharedKeyAccess: false (Storage) │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ Autorización ──────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ✅ RBAC (Role-Based Access Control) para cada recurso │ │ -│ │ ✅ Principio de mínimo privilegio │ │ -│ │ ✅ Roles específicos por tipo de operación │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ Red (modo AILZ) ──────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ✅ Private Endpoints para datos │ │ -│ │ ✅ Subnets dedicadas │ │ -│ │ ✅ DNS privado │ │ -│ │ ✅ Network rules: Deny por defecto │ │ -│ │ ✅ Bypass: AzureServices │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ Datos ─────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ✅ Encriptación en tránsito (HTTPS/TLS) │ │ -│ │ ✅ Encriptación en reposo (Azure managed keys) │ │ -│ │ ✅ Backup continuo (Cosmos DB: 7-30 días) │ │ -│ │ ✅ Soft delete (Blob Storage: 7 días retención) │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -└────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 10. Configuración centralizada - -### 10.1 Azure App Configuration - -Todas las configuraciones de runtime se almacenan en **Azure App Configuration** y se leen tanto por el API como por el Worker: - -``` -App Configuration Store (appcs-${token}) -│ -├── Cosmos DB -│ ├── COSMOS_DB_ENDPOINT: https://cosmos-xxx.documents.azure.com -│ ├── COSMOS_DB_NAME: contentflow -│ ├── COSMOS_DB_CONTAINER_PIPELINES: pipelines -│ ├── COSMOS_DB_CONTAINER_VAULTS: vaults -│ ├── COSMOS_DB_CONTAINER_VAULT_EXECUTIONS: vault_executions -│ ├── COSMOS_DB_CONTAINER_VAULT_EXECUTION_LOCKS: vault_exec_locks -│ ├── COSMOS_DB_CONTAINER_CRAWL_CHECKPOINTS: vault_crawl_checkpoints -│ └── COSMOS_DB_CONTAINER_PIPELINE_EXECUTIONS: pipeline_executions -│ -├── Storage -│ ├── BLOB_STORAGE_ACCOUNT_NAME: stxxxx -│ ├── BLOB_STORAGE_CONTAINER_NAME: content -│ ├── STORAGE_ACCOUNT_WORKER_QUEUE_URL: https://stxxxx.queue.core.windows.net -│ └── STORAGE_WORKER_QUEUE_NAME: contentflow-execution-requests -│ -├── Worker -│ ├── WORKER_ENGINE_API_ENDPOINT: https://worker-xxx.azurecontainerapps.io -│ ├── NUM_PROCESSING_WORKERS: 0 -│ └── NUM_SOURCE_WORKERS: 1 -│ -└── Application - ├── API_SERVER_PORT: 8090 - ├── LOG_LEVEL: DEBUG - └── DEBUG: true -``` - -### 10.2 Variables de entorno por servicio - -| Variable | API | Worker | Descripción | -|----------|:---:|:------:|-------------| -| `COSMOS_DB_ENDPOINT` | ✅ | ✅ | Endpoint de Cosmos DB | -| `COSMOS_DB_NAME` | ✅ | ✅ | Nombre de la base de datos | -| `BLOB_STORAGE_ACCOUNT_NAME` | ✅ | ✅ | Nombre de la cuenta de storage | -| `BLOB_STORAGE_CONTAINER_NAME` | ✅ | ✅ | Nombre del contenedor blob | -| `STORAGE_ACCOUNT_WORKER_QUEUE_URL` | ✅ | ✅ | URL de la cola | -| `STORAGE_WORKER_QUEUE_NAME` | ✅ | ✅ | Nombre de la cola | -| `API_SERVER_PORT` | ✅ | — | Puerto del API (8090) | -| `API_PORT` | — | ✅ | Puerto de health del worker (8099) | -| `NUM_PROCESSING_WORKERS` | — | ✅ | Workers de procesamiento | -| `NUM_SOURCE_WORKERS` | — | ✅ | Workers de entrada | -| `QUEUE_POLL_INTERVAL_SECONDS` | — | ✅ | Intervalo de poll (5s) | -| `DEFAULT_POLLING_INTERVAL_SECONDS` | — | ✅ | Intervalo de descubrimiento (300s) | -| `MAX_TASK_RETRIES` | — | ✅ | Reintentos por tarea (3) | -| `TASK_TIMEOUT_SECONDS` | — | ✅ | Timeout por tarea (600s) | -| `LOG_LEVEL` | ✅ | ✅ | Nivel de logging (DEBUG) | -| `DEBUG` | ✅ | ✅ | Modo debug | - ---- - -## 11. Monitoreo y observabilidad - -``` -┌─────────────────────────────────────────────────────────────┐ -│ STACK DE OBSERVABILIDAD │ -│ │ -│ ┌─ Application Insights ─────────────────────────────────┐ │ -│ │ │ │ -│ │ • Trazas distribuidas entre servicios │ │ -│ │ • Métricas de rendimiento (latencia, errores) │ │ -│ │ • Logs de aplicación (Python + React) │ │ -│ │ • Mapa de dependencias entre componentes │ │ -│ │ • Alertas configurables │ │ -│ │ │ │ -│ └─────────────────────┬───────────────────────────────────┘ │ -│ │ │ -│ ┌─ Log Analytics ─────▼──────────────────────────────────┐ │ -│ │ │ │ -│ │ • Logs de Container Apps (stdout/stderr) │ │ -│ │ • Logs de sistema de Container Apps Environment │ │ -│ │ • Métricas de infraestructura │ │ -│ │ • Consultas KQL personalizables │ │ -│ │ • Retención configurable │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ Fuentes de datos: │ -│ ├── API Service ──────▶ Logs HTTP, latencia, errores │ -│ ├── Worker Service ───▶ Logs de procesamiento, tareas │ -│ ├── Web Service ──────▶ Logs de build, errores client-side │ -│ └── Container Apps ───▶ Escalado, arranques, health checks │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 12. Resumen de dependencias entre componentes - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ MAPA DE DEPENDENCIAS DE CONTENTFLOW │ -│ │ -│ │ -│ contentflow-web ──────────────▶ contentflow-api │ -│ (Frontend) HTTP/REST (Backend) │ -│ │ │ -│ ├──── azure-cosmos (SDK) │ -│ ├──── azure-storage-blob (SDK) │ -│ ├──── azure-storage-queue (SDK) │ -│ ├──── azure-appconfiguration (SDK) │ -│ └──── contentflow-lib (local) │ -│ │ │ -│ contentflow-worker ─────────────────────────────┤ │ -│ (Processing) │ │ -│ │ │ │ -│ ├──── azure-cosmos (SDK) │ │ -│ ├──── azure-storage-queue (SDK) contentflow-lib │ -│ ├──── azure-storage-blob (SDK) (Core Engine) │ -│ └──── contentflow-lib (local) ──┐ │ │ -│ │ ├── Pipeline Engine │ -│ └──────────├── 35+ Ejecutores │ -│ ├── Content Model │ -│ ├── Conectores Azure │ -│ │ ├── Blob Storage │ -│ │ ├── AI Search │ -│ │ ├── Document Intel. │ -│ │ ├── OpenAI │ -│ │ └── Cosmos DB │ -│ └── Utilidades │ -│ │ -│ infra/ (Bicep) │ -│ │ │ -│ └──── Aprovisiona TODOS los recursos de Azure que los │ -│ servicios anteriores consumen │ -│ │ -│ azure.yaml │ -│ │ │ -│ └──── Orquesta el despliegue de infra/ + servicios │ -│ via Azure Developer CLI (azd) │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -> **Este documento refleja el estado de la arquitectura a la fecha de generación. Consulte el código fuente para la información más actualizada.** diff --git a/Analysis/03-use_cases_examples.md b/Analysis/03-use_cases_examples.md deleted file mode 100644 index d2e641f..0000000 --- a/Analysis/03-use_cases_examples.md +++ /dev/null @@ -1,666 +0,0 @@ -# ContentFlow — Casos de Uso y Ejemplos - -> **Catálogo de escenarios implementables con el acelerador ContentFlow**, organizados por industria y función, con los ejecutores recomendados para cada caso. - ---- - -## Índice - -- [1. Procesamiento de facturas y recibos](#1-procesamiento-de-facturas-y-recibos) -- [2. Indexación inteligente de contenido](#2-indexación-inteligente-de-contenido) -- [3. Análisis de contratos](#3-análisis-de-contratos) -- [4. Grafos de conocimiento evolutivos](#4-grafos-de-conocimiento-evolutivos) -- [5. Procesamiento de formularios](#5-procesamiento-de-formularios) -- [6. Cumplimiento normativo y gestión de riesgos](#6-cumplimiento-normativo-y-gestión-de-riesgos) -- [7. Procesamiento de contenido web](#7-procesamiento-de-contenido-web) -- [8. Análisis de imagen y contenido visual](#8-análisis-de-imagen-y-contenido-visual) -- [9. Procesamiento de adjuntos de correo electrónico](#9-procesamiento-de-adjuntos-de-correo-electrónico) -- [10. Ingestión de contenido para GPT-RAG](#10-ingestión-de-contenido-para-gpt-rag) -- [11. Resumen de artículos y reportes](#11-resumen-de-artículos-y-reportes) -- [12. Traducción de contenido multilingüe](#12-traducción-de-contenido-multilingüe) -- [13. Resumen de ejecutores por caso de uso](#13-resumen-de-ejecutores-por-caso-de-uso) -- [14. Pipelines de ejemplo incluidos en el repositorio](#14-pipelines-de-ejemplo-incluidos-en-el-repositorio) - ---- - -## 1. Procesamiento de facturas y recibos - -Automatiza la extracción de datos estructurados de facturas, recibos y órdenes de compra usando Azure Content Understanding. - -**Capacidades**: -- Extraer datos estructurados de facturas usando Azure Content Understanding -- Clasificar documentos por tipo (factura, recibo, orden de compra) -- Validar campos extraídos y señalar anomalías -- Exportar a sistemas ERP o bases de datos - -**Pipeline sugerido**: - -```yaml -name: procesamiento-facturas -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: facturas-entrantes - file_extensions: [".pdf", ".png", ".jpg"] - - - executor: azure_blob_content_retriever - - - executor: azure_content_understanding_extractor - settings: - model_id: prebuilt-invoice - - - executor: content_classifier - settings: - categories: ["factura", "recibo", "orden_de_compra"] - - - executor: field_mapper - settings: - mappings: - vendor_name: "proveedor" - total_amount: "monto_total" - invoice_date: "fecha_factura" - - - executor: azure_blob_output - settings: - blob_container_name: facturas-procesadas -``` - -**Ejecutores clave**: `azure_content_understanding_extractor`, `content_classifier`, `field_mapper`, `azure_blob_output` - ---- - -## 2. Indexación inteligente de contenido - -Procesa documentos, imágenes, audio y video para generar embeddings vectoriales e indexar en Azure AI Search, habilitando búsqueda semántica empresarial. - -**Capacidades**: -- Procesar documentos en múltiples formatos (PDF, Word, Excel, PowerPoint) -- Generar vectores de embedding para búsqueda semántica -- Indexar contenido en Azure AI Search -- Habilitar búsqueda semántica en todo el contenido empresarial - -**Pipeline sugerido**: - -```yaml -name: indexacion-inteligente -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documentos-empresa - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - settings: - model_id: prebuilt-layout - - - executor: recursive_text_chunker - settings: - chunk_size: 1000 - chunk_overlap: 200 - - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - - executor: ai_search_index_output - settings: - index_name: contenido-empresarial -``` - -**Ejecutores clave**: `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `ai_search_index_output` - ---- - -## 3. Análisis de contratos - -Extrae cláusulas clave, identifica partes involucradas, fechas, montos, y genera resúmenes automáticos para revisión legal. - -**Capacidades**: -- Extraer cláusulas clave y obligaciones contractuales -- Identificar partes, fechas y valores monetarios -- Señalar términos riesgosos usando análisis de IA -- Generar resúmenes para revisión legal - -**Pipeline sugerido**: - -```yaml -name: analisis-contratos -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: contratos - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: entity_extractor - settings: - entity_types: ["persona", "organizacion", "fecha", "monto"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1 - prompt: | - Analiza este contrato y extrae: - 1. Cláusulas clave y obligaciones - 2. Términos riesgosos o inusuales - 3. Fechas límite y vencimientos - 4. Valores monetarios y condiciones de pago - - - executor: text_summarizer - settings: - max_length: 500 - style: "resumen ejecutivo legal" - - - executor: azure_blob_output - settings: - blob_container_name: contratos-analizados -``` - -**Ejecutores clave**: `pdf_extractor`, `entity_extractor`, `azure_openai_agent`, `text_summarizer` - ---- - -## 4. Grafos de conocimiento evolutivos - -Construye grafos de conocimiento que mapean entidades, relaciones y estructuras organizativas a partir de documentos corporativos. - -**Capacidades**: -- Extraer entidades (personas, organizaciones, productos, ubicaciones) -- Identificar relaciones (trabaja_en, gestiona, ubicado_en) -- Mapear estructuras organizativas -- Descubrir conexiones ocultas entre documentos - -**Pipeline sugerido**: - -```yaml -name: grafo-conocimiento -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documentos-corporativos - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - - - executor: recursive_text_chunker - settings: - chunk_size: 2000 - - - executor: entity_extractor - settings: - entity_types: ["persona", "organizacion", "producto", "ubicacion"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1 - prompt: | - A partir de las entidades extraídas, identifica relaciones como: - - trabaja_en, gestiona, reporta_a - - ubicado_en, opera_en - - provee, compra_a, compite_con - Devuelve las relaciones en formato estructurado. - - - executor: azure_blob_output - settings: - blob_container_name: grafos-conocimiento -``` - -**Ejecutores clave**: `entity_extractor`, `azure_openai_agent`, `document_set_initializer`, `cross_document_comparison` - ---- - -## 5. Procesamiento de formularios - -Automatiza la captura y validación de datos de formularios, solicitudes, reclamaciones y aplicaciones. - -**Capacidades**: -- Procesar solicitudes, reclamaciones y formularios -- Extraer campos estructurados con ejecutores de IA -- Validar completitud y precisión de los datos -- Enrutar al sistema de línea de negocio (LoB) apropiado - -**Pipeline sugerido**: - -```yaml -name: procesamiento-formularios -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: formularios-entrantes - - - executor: azure_blob_content_retriever - - - executor: azure_content_understanding_extractor - settings: - model_id: prebuilt-document - - - executor: field_selector - settings: - fields: ["nombre", "fecha", "tipo_solicitud", "monto", "descripcion"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1-mini - prompt: | - Valida los campos extraídos: - - ¿Están todos los campos obligatorios presentes? - - ¿Los formatos son correctos (fechas, montos)? - - Señala campos faltantes o inconsistentes. - - - executor: content_classifier - settings: - categories: ["solicitud_credito", "reclamacion_seguro", "solicitud_empleo"] - - - executor: azure_blob_output - settings: - blob_container_name: formularios-procesados -``` - -**Ejecutores clave**: `azure_content_understanding_extractor`, `field_selector`, `content_classifier`, `azure_openai_agent` - ---- - -## 6. Cumplimiento normativo y gestión de riesgos - -Detecta información personal identificable (PII), clasifica documentos por sensibilidad y señala problemas de cumplimiento. - -**Capacidades**: -- Detectar PII (Información Personalmente Identificable) -- Clasificar documentos por nivel de sensibilidad -- Señalar problemas de cumplimiento normativo -- Extraer entidades clave y temas sensibles - -**Pipeline sugerido**: - -```yaml -name: cumplimiento-riesgos -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documentos-revision - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: pii_detector - settings: - pii_types: ["nombre", "email", "telefono", "numero_seguro_social", - "tarjeta_credito", "direccion", "fecha_nacimiento"] - - - executor: content_classifier - settings: - categories: ["publico", "interno", "confidencial", "restringido"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1 - prompt: | - Analiza el documento para cumplimiento normativo: - - ¿Contiene información regulada (GDPR, HIPAA, PCI-DSS)? - - ¿Se manejan datos sensibles correctamente? - - Identifica riesgos potenciales de cumplimiento. - Genera un reporte con nivel de riesgo: bajo, medio, alto, crítico. - - - executor: azure_blob_output - settings: - blob_container_name: reportes-cumplimiento -``` - -**Ejecutores clave**: `pii_detector`, `content_classifier`, `entity_extractor`, `azure_openai_agent` - ---- - -## 7. Procesamiento de contenido web - -Extrae, traduce y corrige contenido de sitios web para alimentar soluciones de chat basadas en RAG o sitios multilingües. - -**Capacidades**: -- Extraer contenido de sitios web para soluciones de chat basadas en RAG -- Traducir contenido web para experiencias multilingües -- Identificar y corregir errores ortográficos y de contenido - -**Pipeline sugerido**: - -```yaml -name: procesamiento-web -steps: - - executor: web_scraper - settings: - urls: - - "https://docs.empresa.com" - - "https://soporte.empresa.com" - max_depth: 3 - selectors: ["article", "main", ".content"] - - - executor: recursive_text_chunker - settings: - chunk_size: 1500 - - - executor: language_detector - - - executor: content_translator - settings: - target_language: "es" - - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - - executor: ai_search_index_output - settings: - index_name: contenido-web-rag -``` - -**Ejecutores clave**: `web_scraper`, `language_detector`, `content_translator`, `azure_openai_embeddings` - ---- - -## 8. Análisis de imagen y contenido visual - -Digitaliza notas clínicas manuscritas, extrae texto de reportes visuales y analiza imágenes de daños para procesamiento automatizado de reclamaciones. - -**Capacidades**: - -| Industria | Aplicación | -|-----------|-----------| -| **Salud** | Digitalizar notas clínicas manuscritas, extraer texto de reportes, generar descripciones de imágenes médicas para registros electrónicos de salud y cumplimiento de accesibilidad | -| **Seguros** | Procesar formularios manuscritos de reclamaciones, extraer datos de fotos de accidentes, analizar imágenes de evaluación de daños para procesamiento automatizado | - -**Pipeline sugerido**: - -```yaml -name: analisis-visual-seguros -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: reclamaciones-fotos - file_extensions: [".jpg", ".png", ".pdf"] - - - executor: azure_blob_content_retriever - - - executor: azure_content_understanding_extractor - settings: - model_id: prebuilt-document - - - executor: azure_openai_agent - settings: - model: gpt-4.1 - prompt: | - Analiza esta imagen/documento de reclamación de seguro: - - Identifica el tipo de daño visible - - Estima la severidad (leve, moderado, severo) - - Extrae información del formulario de reclamación - - Señala cualquier inconsistencia - - - executor: content_classifier - settings: - categories: ["vehicular", "propiedad", "salud", "vida"] - - - executor: azure_blob_output - settings: - blob_container_name: reclamaciones-procesadas -``` - -**Ejecutores clave**: `azure_content_understanding_extractor`, `azure_openai_agent`, `content_classifier` - ---- - -## 9. Procesamiento de adjuntos de correo electrónico - -Analiza hilos de correo para extraer tareas, detectar sentimiento y enrutar automáticamente a los equipos adecuados. - -**Capacidades**: - -| Industria | Aplicación | -|-----------|-----------| -| **Servicio al cliente** | Analizar hilos de soporte para extraer acciones pendientes, detectar sentimiento del cliente, y enrutar automáticamente problemas urgentes a los equipos apropiados | -| **Legal** | Categorizar automáticamente correspondencia legal, identificar puntos de negociación, extraer fechas límite y obligaciones de hilos de correo | - -**Pipeline sugerido**: - -```yaml -name: procesamiento-correos -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: correos-entrantes - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: sentiment_analyser - settings: - granularity: "documento" - - - executor: entity_extractor - settings: - entity_types: ["persona", "fecha", "organizacion", "accion"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1-mini - prompt: | - Del correo electrónico, extrae: - 1. Acciones pendientes y responsables - 2. Fechas límite mencionadas - 3. Nivel de urgencia (bajo, medio, alto, crítico) - 4. Tema principal y categoría - - - executor: content_classifier - settings: - categories: ["soporte_tecnico", "facturacion", "queja", - "solicitud_informacion", "legal"] - - - executor: azure_blob_output - settings: - blob_container_name: correos-procesados -``` - -**Ejecutores clave**: `sentiment_analyser`, `entity_extractor`, `azure_openai_agent`, `content_classifier` - ---- - -## 10. Ingestión de contenido para GPT-RAG - -Construye bases de conocimiento buscables a partir de documentos empresariales, procesando múltiples formatos y creando embeddings para búsqueda semántica en soluciones de chat con IA. - -**Capacidades**: -- Construir bases de conocimiento buscables desde documentos empresariales -- Procesar contenido en múltiples formatos para chat impulsado por IA -- Crear embeddings para búsqueda semántica - -**Pipeline sugerido**: - -```yaml -name: ingestion-gpt-rag -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: base-conocimiento - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - settings: - model_id: prebuilt-layout - - - executor: recursive_text_chunker - settings: - chunk_size: 800 - chunk_overlap: 150 - - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - - executor: gptrag_search_index_document_generator - settings: - index_name: rag-knowledge-base - - - executor: ai_search_index_output - settings: - index_name: rag-knowledge-base -``` - -**Ejecutores clave**: `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `gptrag_search_index_document_generator` - ---- - -## 11. Resumen de artículos y reportes - -Genera resúmenes concisos de artículos de noticias, reportes ejecutivos y snippets para redes sociales. - -**Capacidades**: -- Resumir artículos de noticias para boletines internos -- Crear resúmenes ejecutivos de reportes extensos -- Generar snippets para publicaciones en redes sociales - -**Pipeline sugerido**: - -```yaml -name: resumen-articulos -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: articulos-reportes - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: language_detector - - - executor: keyword_extractor - settings: - max_keywords: 10 - - - executor: text_summarizer - settings: - max_length: 300 - style: "resumen ejecutivo" - - - executor: azure_openai_agent - settings: - model: gpt-4.1-mini - prompt: | - A partir del resumen generado, crea: - 1. Un titular de una línea - 2. Un resumen de 3 oraciones para boletín - 3. Un snippet para redes sociales (max 280 caracteres) - - - executor: azure_blob_output - settings: - blob_container_name: resumenes-generados -``` - -**Ejecutores clave**: `keyword_extractor`, `text_summarizer`, `language_detector`, `azure_openai_agent` - ---- - -## 12. Traducción de contenido multilingüe - -Traduce documentación de productos, marketing y bases de conocimiento para alcanzar audiencias globales. - -**Capacidades**: -- Traducir documentación de productos a múltiples idiomas -- Localizar contenido de marketing para diferentes regiones -- Crear bases de conocimiento multilingües - -**Pipeline sugerido**: - -```yaml -name: traduccion-multilingue -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: contenido-original - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: language_detector - - - executor: recursive_text_chunker - settings: - chunk_size: 2000 - - - executor: content_translator - settings: - target_languages: ["es", "fr", "de", "pt", "ja"] - - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - - executor: ai_search_index_output - settings: - index_name: contenido-multilingue -``` - -**Ejecutores clave**: `language_detector`, `content_translator`, `azure_openai_embeddings`, `ai_search_index_output` - ---- - -## 13. Resumen de ejecutores por caso de uso - -| Caso de uso | Ejecutores principales | -|---|---| -| **Facturas y recibos** | `azure_content_understanding_extractor`, `content_classifier`, `field_mapper` | -| **Indexación inteligente** | `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `ai_search_index_output` | -| **Análisis de contratos** | `pdf_extractor`, `entity_extractor`, `azure_openai_agent`, `text_summarizer` | -| **Grafos de conocimiento** | `entity_extractor`, `azure_openai_agent`, `cross_document_comparison` | -| **Formularios** | `azure_content_understanding_extractor`, `field_selector`, `content_classifier` | -| **Cumplimiento y riesgos** | `pii_detector`, `content_classifier`, `azure_openai_agent` | -| **Contenido web** | `web_scraper`, `content_translator`, `azure_openai_embeddings` | -| **Análisis visual** | `azure_content_understanding_extractor`, `azure_openai_agent` | -| **Correo electrónico** | `sentiment_analyser`, `entity_extractor`, `content_classifier` | -| **GPT-RAG** | `azure_document_intelligence_extractor`, `recursive_text_chunker`, `gptrag_search_index_document_generator` | -| **Resúmenes** | `keyword_extractor`, `text_summarizer`, `azure_openai_agent` | -| **Traducción** | `language_detector`, `content_translator`, `azure_openai_embeddings` | - ---- - -## 14. Pipelines de ejemplo incluidos en el repositorio - -El repositorio incluye más de **20 pipelines de ejemplo** listos para ejecutar en la carpeta `contentflow-lib/samples/`: - -| Carpeta | Nombre | Descripción | -|---------|--------|-------------| -| `01-simple/` | Pipeline simple | Pipeline básico de extracción de texto | -| `02-batch-processing/` | Procesamiento por lotes | Procesamiento de múltiples documentos | -| `03-pdf-extractor_chunker/` | PDF + Chunking | Extracción de PDF y división en fragmentos | -| `04-word-extractor/` | Extractor Word | Procesamiento de documentos .docx | -| `05-powerpoint-extractor/` | Extractor PowerPoint | Procesamiento de presentaciones .pptx | -| `06-ai-analysis/` | Análisis con IA | Análisis inteligente con GPT | -| `07-embeddings/` | Embeddings | Generación de vectores de embedding | -| `08-content-understanding/` | Content Understanding | Azure Content Understanding | -| `09-blob-input/` | Entrada desde Blob | Descubrimiento de contenido en Blob Storage | -| `10-table-row-splitter/` | Divisor de filas | Procesamiento fila por fila de tablas | -| `11-excel-extractor/` | Extractor Excel | Procesamiento de hojas de cálculo | -| `12-field-transformation/` | Transformación de campos | Mapeo y transformación de datos | -| `13-blob-output-sample/` | Salida a Blob | Escritura de resultados a Blob Storage | -| `14-gpt-rag-ingestion/` | Ingestión GPT-RAG | Pipeline completo para RAG | -| `15-document-analysis/` | Análisis de documentos | Análisis completo con Document Intelligence | -| `16-spreadsheet-pipeline/` | Pipeline de hojas de cálculo | Procesamiento de Excel end-to-end | -| `17-knowledge-graph/` | Grafo de conocimiento | Extracción de entidades y relaciones | -| `18-web-scraping/` | Web scraping | Extracción de contenido web | -| `19-sub-pipelines/` | Sub-pipelines | Pipelines anidados | -| `20-document-set-static/` | Conjunto estático | Procesamiento de conjuntos de documentos | -| `21-document-set-comparison/` | Comparación de documentos | Comparación cruzada de documentos | -| `22-document-set-dynamic/` | Conjunto dinámico | Conjuntos de documentos dinámicos | -| `23-inline-document-set/` | Conjunto inline | Conjuntos definidos en línea | -| `27-subpipeline-processing/` | Sub-pipeline avanzado | Procesamiento con sub-pipelines | -| `28-advanced-batch/` | Lotes avanzado | Procesamiento por lotes avanzado | -| `32-parallel-processing/` | Procesamiento paralelo | Flujos de trabajo en paralelo | -| `44-conditional-routing/` | Enrutamiento condicional | Lógica condicional en pipelines | - ---- - -> Cada caso de uso se puede implementar combinando los **35+ ejecutores** disponibles en ContentFlow. Los pipelines YAML son completamente configurables y se pueden diseñar visualmente desde la interfaz web o editarse directamente en el editor YAML integrado. diff --git a/Analysis/04-ai_lz_options.md b/Analysis/04-ai_lz_options.md deleted file mode 100644 index 07aeb4d..0000000 --- a/Analysis/04-ai_lz_options.md +++ /dev/null @@ -1,1224 +0,0 @@ -# Guía de Despliegue: ContentFlow en Modo AI Landing Zone Integrated - -> **Documento técnico operativo** - Instrucciones paso a paso para desplegar ContentFlow con conectividad privada usando una AI Landing Zone mínima en un ambiente de demo con suscripción única. - ---- - -## Tabla de Contenidos - -1. [Resumen Ejecutivo](#1-resumen-ejecutivo) -2. [¿Qué es el Modo AILZ-Integrated?](#2-qué-es-el-modo-ailz-integrated) -3. [Arquitectura de Red: Basic vs AILZ](#3-arquitectura-de-red-basic-vs-ailz) -4. [Prerrequisitos Generales](#4-prerrequisitos-generales) -5. [El Problema: Demo con Suscripción Única](#5-el-problema-demo-con-suscripción-única) -6. [Solución: AI Landing Zone Mínima ("Mini-AILZ")](#6-solución-ai-landing-zone-mínima-mini-ailz) -7. [Paso a Paso: Crear la Mini-AILZ](#7-paso-a-paso-crear-la-mini-ailz) -8. [Paso a Paso: Desplegar ContentFlow en Modo AILZ](#8-paso-a-paso-desplegar-contentflow-en-modo-ailz) -9. [Acceso y Pruebas de Conectividad Privada](#9-acceso-y-pruebas-de-conectividad-privada) -10. [Mapa Completo de Parámetros](#10-mapa-completo-de-parámetros) -11. [Cambios por Recurso en Modo AILZ](#11-cambios-por-recurso-en-modo-ailz) -12. [Troubleshooting](#12-troubleshooting) -13. [Costos Estimados de la Mini-AILZ](#13-costos-estimados-de-la-mini-ailz) -14. [Limpieza de Recursos](#14-limpieza-de-recursos) - ---- - -## 1. Resumen Ejecutivo - -ContentFlow soporta dos modos de despliegue: - -| Aspecto | `basic` | `ailz-integrated` | -|---|---|---| -| Endpoints | Públicos (internet) | Privados (VNet) | -| Red | Sin VNet | VNet + Private Endpoints | -| DNS | Resolución pública | Private DNS Zones | -| ACR SKU | Standard | **Premium** (requerido para PE) | -| Firewall de recursos | `Allow` (todo abierto) | `Deny` (solo PE) | -| Ingress de Container Apps | Externo (público) | **Interno** (solo VNet) | -| Acceso a la UI Web | Navegador directo | Requiere VPN/JumpBox | - -**Escenario de este documento:** Tienes **una sola suscripción de Azure** para demos y quieres probar el modo `ailz-integrated` completo. Normalmente, la AI Landing Zone ya existe como infraestructura compartida enterprise. Aquí crearemos una **versión mínima ("Mini-AILZ")** en la misma suscripción para simular ese escenario. - ---- - -## 2. ¿Qué es el Modo AILZ-Integrated? - -En un entorno enterprise real, el **AI Landing Zone** es una infraestructura de red pre-provisionada por el equipo de plataforma que incluye: - -- **Virtual Network (VNet)** con subredes dedicadas -- **Private DNS Zones** para resolución de nombres internos -- **Network Security Groups (NSGs)** y políticas de seguridad -- **JumpBox VM** para acceso administrativo -- **Shared Log Analytics** y **Application Insights** centralizados -- **Azure Firewall** o NVA para control de tráfico (enterprise completo) - -ContentFlow en modo `ailz-integrated` **no crea red propia**. En su lugar: - -1. **Reutiliza** la VNet y subredes existentes del AILZ -2. **Crea Private Endpoints** en la subred dedicada (`pe-subnet`) -3. **Registra registros DNS** en las Private DNS Zones existentes -4. **Configura todos los recursos** con `publicNetworkAccess: Disabled` -5. **Integra Container Apps Environment** con la VNet (modo interno) - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ AI Landing Zone (VNet) │ -│ │ -│ ┌──────────────────────┐ ┌────────────────────────────────────┐ │ -│ │ pe-subnet (/27+) │ │ aca-env-subnet (/23) │ │ -│ │ │ │ │ │ -│ │ ● Storage PE (blob) │ │ ┌──────────────────────────────┐ │ │ -│ │ ● Storage PE (queue)│ │ │ Container Apps Environment │ │ │ -│ │ ● Cosmos DB PE │ │ │ (Internal Load Balancer) │ │ │ -│ │ ● App Config PE │ │ │ │ │ │ -│ │ ● ACR PE (Premium) │ │ │ ┌─────┐ ┌──────┐ ┌─────┐ │ │ │ -│ │ ● AI Foundry PE │ │ │ │ API │ │Worker│ │ Web │ │ │ │ -│ │ │ │ │ └─────┘ └──────┘ └─────┘ │ │ │ -│ └──────────────────────┘ │ └──────────────────────────────┘ │ │ -│ └────────────────────────────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────────────────────────────────────┐ │ -│ │ jumpbox-vm │ │ Private DNS Zones (6 requeridas) │ │ -│ │ (acceso) │ │ ● privatelink.blob.core.windows.net │ │ -│ └──────────────┘ │ ● privatelink.documents.azure.com │ │ -│ │ ● privatelink.azconfig.io │ │ -│ │ ● privatelink.azurecr.io │ │ -│ │ ● privatelink.cognitiveservices.azure.com │ │ -│ │ ● privatelink.azurecontainerapps.io │ │ -│ └──────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 3. Arquitectura de Red: Basic vs AILZ - -### Modo Basic (Público) - -``` -Internet ──→ Container Apps (API/Web) ──→ Storage Account (público) - ──→ Cosmos DB (público) - ──→ App Config (público) - ──→ ACR Standard (público) - ──→ AI Foundry (público) -``` - -- Todos los recursos tienen endpoints públicos -- Accesibles desde cualquier IP -- Network ACLs: `defaultAction: Allow` - -### Modo AILZ-Integrated (Privado) - -``` -JumpBox/VPN ──→ VNet ──→ Container Apps (interno) ──→ Storage PE (privado) - ──→ Cosmos PE (privado) - ──→ App Config PE (privado) - ──→ ACR PE Premium (privado) - ──→ AI Foundry PE (privado) -``` - -- Todos los recursos: `publicNetworkAccess: Disabled` -- Network ACLs: `defaultAction: Deny`, `bypass: AzureServices` -- Acceso solo vía Private Endpoints dentro de la VNet -- Container Apps: `internal: true`, solo accesible desde VNet - ---- - -## 4. Prerrequisitos Generales - -### Herramientas Requeridas - -```bash -# Verificar instalación -az --version # Azure CLI 2.60+ -azd version # Azure Developer CLI 1.5+ -docker --version # Docker Desktop (para build de containers) -``` - -### Permisos Azure Requeridos - -| Permiso | Escenario | Razón | -|---|---|---| -| `Contributor` | Resource Group de ContentFlow | Crear todos los recursos | -| `Network Contributor` | Resource Group de la VNet | Crear Private Endpoints en subredes | -| `Private DNS Zone Contributor` | Resource Group de DNS Zones | Crear registros A para PEs | -| `User Access Administrator` | Resource Group | Asignar RBAC a Managed Identity | - -> **Nota:** En el escenario Mini-AILZ (suscripción única), si usas un Resource Group separado para la red, necesitas permisos en ambos RGs. - -### Registro de Providers - -```bash -# Asegurar que estos providers estén registrados -az provider register --namespace Microsoft.App -az provider register --namespace Microsoft.ContainerRegistry -az provider register --namespace Microsoft.DocumentDB -az provider register --namespace Microsoft.Network -az provider register --namespace Microsoft.CognitiveServices -az provider register --namespace Microsoft.Storage -az provider register --namespace Microsoft.AppConfiguration -``` - ---- - -## 5. El Problema: Demo con Suscripción Única - -En un entorno enterprise real: - -``` -┌──────────────────────────────┐ ┌──────────────────────────────┐ -│ Suscripción: Platform │ │ Suscripción: Workloads │ -│ │ │ │ -│ RG: rg-ailz-network │ │ RG: rg-contentflow │ -│ ├── VNet │ │ ├── Storage + PE │ -│ ├── Private DNS Zones │ │ ├── Cosmos DB + PE │ -│ ├── NSGs │ │ ├── Container Apps (int) │ -│ ├── Azure Firewall │ │ ├── ACR Premium + PE │ -│ └── JumpBox VM │ │ ├── App Config + PE │ -│ │ │ └── AI Foundry + PE │ -│ (Manejado por Platform Team)│ │ (Manejado por App Team) │ -└──────────────────────────────┘ └──────────────────────────────┘ -``` - -**Tu situación:** Solo tienes **una suscripción**. No existe un AILZ pre-provisionado. Necesitas: - -1. **Simular** la infraestructura de red que normalmente provee el Platform Team -2. **Desplegar** ContentFlow en modo `ailz-integrated` apuntando a esa red -3. **Acceder** a los servicios internos para validar la demo - ---- - -## 6. Solución: AI Landing Zone Mínima ("Mini-AILZ") - -Crearemos la infraestructura mínima necesaria en un **Resource Group separado** dentro de la misma suscripción: - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Suscripción Única de Demo │ -│ │ -│ ┌──────────────────────────────┐ ┌─────────────────────────────┐ │ -│ │ RG: rg-mini-ailz │ │ RG: rg-contentflow-ailz │ │ -│ │ │ │ │ │ -│ │ ● VNet (10.0.0.0/16) │ │ ● Storage + PE │ │ -│ │ ├── pe-subnet /27 │ │ ● Cosmos DB + PE │ │ -│ │ ├── aca-env-subnet /23 │ │ ● ACR Premium + PE │ │ -│ │ └── jumpbox-subnet /27 │ │ ● App Config + PE │ │ -│ │ │ │ ● Container Apps Env (int) │ │ -│ │ ● 6 Private DNS Zones │ │ ● Container Apps (API/ │ │ -│ │ (vinculadas a la VNet) │ │ Worker/Web) │ │ -│ │ │ │ ● AI Foundry + PE │ │ -│ │ ● JumpBox VM (B2s) │ │ ● App Insights (nuevo) │ │ -│ │ (con Bastion o IP pública │ │ ● Log Analytics (nuevo) │ │ -│ │ temporal para demo) │ │ ● User Assigned Identity │ │ -│ │ │ │ │ │ -│ │ ● Log Analytics (opcional) │ │ │ │ -│ │ ● App Insights (opcional) │ │ │ │ -│ └──────────────────────────────┘ └─────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### ¿Por qué un RG separado? - -- **Simula el escenario real**: Red y aplicación en RGs diferentes -- **Limpieza fácil**: Puedes borrar `rg-contentflow-ailz` sin afectar la red -- **Permisos realistas**: Puedes validar que los permisos cross-RG funcionan -- **Reutilizable**: La Mini-AILZ puede servir para otras demos de servicios con Private Endpoints - ---- - -## 7. Paso a Paso: Crear la Mini-AILZ - -### 7.1 Definir Variables - -```bash -# === CONFIGURACIÓN === -# Ajusta estos valores a tu ambiente -SUBSCRIPTION_ID=$(az account show --query id -o tsv) -LOCATION="eastus2" # Tu región preferida -AILZ_RG="rg-mini-ailz" # RG para la infraestructura de red -CF_RG="rg-contentflow-ailz" # RG para ContentFlow (lo crea azd) -VNET_NAME="vnet-ailz-demo" -VNET_PREFIX="10.0.0.0/16" - -# Subredes -PE_SUBNET_NAME="pe-subnet" # Nombre esperado por ContentFlow -PE_SUBNET_PREFIX="10.0.1.0/27" # /27 = 32 IPs (suficiente para ~25 PEs) -ACA_SUBNET_NAME="aca-env-subnet" # Nombre esperado por ContentFlow -ACA_SUBNET_PREFIX="10.0.16.0/23" # /23 = 512 IPs (requerido por Container Apps) -JUMPBOX_SUBNET_NAME="jumpbox-subnet" -JUMPBOX_SUBNET_PREFIX="10.0.2.0/27" # /27 = 32 IPs -``` - -### 7.2 Crear Resource Group de Red - -```bash -az group create \ - --name $AILZ_RG \ - --location $LOCATION \ - --tags purpose=mini-ailz owner=demo -``` - -### 7.3 Crear Virtual Network con Subredes - -```bash -# Crear VNet -az network vnet create \ - --resource-group $AILZ_RG \ - --name $VNET_NAME \ - --address-prefix $VNET_PREFIX \ - --location $LOCATION \ - --tags purpose=mini-ailz - -# Subred para Private Endpoints -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name $PE_SUBNET_NAME \ - --address-prefix $PE_SUBNET_PREFIX - -# Subred para Container Apps Environment -# IMPORTANTE: Container Apps necesita /23 mínimo y la subred debe estar delegada -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name $ACA_SUBNET_NAME \ - --address-prefix $ACA_SUBNET_PREFIX - -# Subred para JumpBox -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name $JUMPBOX_SUBNET_NAME \ - --address-prefix $JUMPBOX_SUBNET_PREFIX -``` - -> **Nota sobre el tamaño de subred para Container Apps:** Container Apps Environment en modo VNet-integrated requiere una subred de al menos `/23` (512 direcciones). Esto es un requisito de la plataforma para manejar la infraestructura interna del entorno. - -### 7.4 Crear las 6 Private DNS Zones Requeridas - -Cada zona debe vincularse (link) a la VNet para que la resolución DNS funcione: - -```bash -# Lista de zonas requeridas por ContentFlow -DNS_ZONES=( - "privatelink.blob.core.windows.net" - "privatelink.documents.azure.com" - "privatelink.azconfig.io" - "privatelink.azurecr.io" - "privatelink.cognitiveservices.azure.com" - "privatelink.azurecontainerapps.io" -) - -# Crear cada zona y vincularla a la VNet -for ZONE in "${DNS_ZONES[@]}"; do - echo "Creando DNS Zone: $ZONE" - - # Crear la zona - az network private-dns zone create \ - --resource-group $AILZ_RG \ - --name "$ZONE" \ - --tags purpose=mini-ailz - - # Vincular la zona a la VNet (CRÍTICO - sin esto la resolución DNS no funciona) - LINK_NAME=$(echo "$ZONE" | sed 's/\./-/g')-link - az network private-dns link vnet create \ - --resource-group $AILZ_RG \ - --zone-name "$ZONE" \ - --name "$LINK_NAME" \ - --virtual-network "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" \ - --registration-enabled false -done - -echo "✓ 6 Private DNS Zones creadas y vinculadas a la VNet" -``` - -### 7.5 (Opcional) Crear Private DNS Zone para Key Vault - -```bash -# Solo si planeas usar Key Vault con Private Endpoint -az network private-dns zone create \ - --resource-group $AILZ_RG \ - --name "privatelink.vaultcore.azure.net" \ - --tags purpose=mini-ailz - -az network private-dns link vnet create \ - --resource-group $AILZ_RG \ - --zone-name "privatelink.vaultcore.azure.net" \ - --name "privatelink-vaultcore-azure-net-link" \ - --virtual-network "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" \ - --registration-enabled false -``` - -### 7.6 Crear JumpBox VM - -La JumpBox es necesaria para acceder a los servicios internos después del despliegue. Tienes **tres opciones**: - -#### Opción A: JumpBox con Azure Bastion (Recomendada para Demo) - -Azure Bastion provee acceso seguro RDP/SSH sin exponer IP pública en la VM. - -```bash -# Crear subred para Azure Bastion (REQUIERE nombre exacto y mínimo /26) -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name "AzureBastionSubnet" \ - --address-prefix "10.0.3.0/26" - -# Crear IP pública para Bastion -az network public-ip create \ - --resource-group $AILZ_RG \ - --name "pip-bastion-demo" \ - --sku Standard \ - --allocation-method Static \ - --location $LOCATION - -# Crear Azure Bastion (Developer SKU - más económico) -az network bastion create \ - --resource-group $AILZ_RG \ - --name "bastion-demo" \ - --public-ip-address "pip-bastion-demo" \ - --vnet-name $VNET_NAME \ - --sku Developer \ - --location $LOCATION - -# Crear la JumpBox VM (sin IP pública - Bastion proporciona acceso) -az vm create \ - --resource-group $AILZ_RG \ - --name "jmp-ailz-demo" \ - --image "MicrosoftWindowsDesktop:windows-11:win11-24h2-pro:latest" \ - --size "Standard_B2ms" \ - --vnet-name $VNET_NAME \ - --subnet $JUMPBOX_SUBNET_NAME \ - --public-ip-address "" \ - --admin-username "azureuser" \ - --admin-password "$(openssl rand -base64 16)A1!" \ - --tags role=jumpbox purpose=mini-ailz \ - --location $LOCATION -``` - -> **Importante:** Anota la contraseña generada. También puedes usar `--admin-password` con un valor específico que controles. - -#### Opción B: JumpBox con IP Pública Temporal (Más Simple) - -```bash -# JumpBox con IP pública (solo para demo, desactivar después) -az vm create \ - --resource-group $AILZ_RG \ - --name "jmp-ailz-demo" \ - --image "MicrosoftWindowsDesktop:windows-11:win11-24h2-pro:latest" \ - --size "Standard_B2ms" \ - --vnet-name $VNET_NAME \ - --subnet $JUMPBOX_SUBNET_NAME \ - --admin-username "azureuser" \ - --admin-password "TuPasswordSeguro123!" \ - --tags role=jumpbox purpose=mini-ailz \ - --nsg-rule RDP \ - --location $LOCATION -``` - -> **Seguridad:** Restringe RDP solo a tu IP: -> ```bash -> MY_IP=$(curl -s https://api.ipify.org) -> az network nsg rule update \ -> --resource-group $AILZ_RG \ -> --nsg-name "jmp-ailz-demoNSG" \ -> --name "rdp" \ -> --source-address-prefixes "$MY_IP/32" -> ``` - -#### Opción C: JumpBox Linux (Más Económica) - -```bash -az vm create \ - --resource-group $AILZ_RG \ - --name "jmp-ailz-demo" \ - --image "Ubuntu2404" \ - --size "Standard_B2s" \ - --vnet-name $VNET_NAME \ - --subnet $JUMPBOX_SUBNET_NAME \ - --admin-username "azureuser" \ - --generate-ssh-keys \ - --tags role=jumpbox purpose=mini-ailz \ - --public-ip-address "" \ - --location $LOCATION -``` - -### 7.7 (Opcional) Crear Observabilidad Compartida - -Si quieres simular el escenario enterprise donde Log Analytics y App Insights son compartidos: - -```bash -# Log Analytics Workspace compartido -az monitor log-analytics workspace create \ - --resource-group $AILZ_RG \ - --workspace-name "log-ailz-shared" \ - --sku PerGB2018 \ - --location $LOCATION \ - --tags purpose=mini-ailz - -# Application Insights compartido -LOG_WS_ID=$(az monitor log-analytics workspace show \ - --resource-group $AILZ_RG \ - --workspace-name "log-ailz-shared" \ - --query id -o tsv) - -az monitor app-insights component create \ - --resource-group $AILZ_RG \ - --app "appi-ailz-shared" \ - --location $LOCATION \ - --workspace "$LOG_WS_ID" \ - --tags purpose=mini-ailz -``` - -> **Nota:** Si no creas estos recursos, ContentFlow en modo `ailz-integrated` **creará sus propios** Log Analytics y App Insights (el código Bicep lo contempla). Solo necesitas crearlos si quieres probar el escenario de observabilidad compartida. - -### 7.8 Verificar la Mini-AILZ - -```bash -echo "=== Verificación de Mini-AILZ ===" - -echo "VNet:" -az network vnet show -g $AILZ_RG -n $VNET_NAME --query "{name:name, address:addressSpace.addressPrefixes[0]}" -o table - -echo "" -echo "Subredes:" -az network vnet subnet list -g $AILZ_RG --vnet-name $VNET_NAME --query "[].{name:name, prefix:addressPrefix}" -o table - -echo "" -echo "Private DNS Zones:" -az network private-dns zone list -g $AILZ_RG --query "[].{name:name, links:numberOfVirtualNetworkLinks}" -o table - -echo "" -echo "JumpBox VM:" -az vm list -g $AILZ_RG --query "[].{name:name, size:hardwareProfile.vmSize, tags:tags.role}" -o table -``` - -Salida esperada: -``` -=== Verificación de Mini-AILZ === -VNet: -Name Address ------------ ---------- -vnet-ailz-demo 10.0.0.0/16 - -Subredes: -Name Prefix ------------ ---------- -pe-subnet 10.0.1.0/27 -aca-env-subnet 10.0.16.0/23 -jumpbox-subnet 10.0.2.0/27 - -Private DNS Zones: -Name Links ---------------------------------------------- ----- -privatelink.blob.core.windows.net 1 -privatelink.documents.azure.com 1 -privatelink.azconfig.io 1 -privatelink.azurecr.io 1 -privatelink.cognitiveservices.azure.com 1 -privatelink.azurecontainerapps.io 1 - -JumpBox VM: -Name Size Tags ------------ ----------- -------- -jmp-ailz-demo Standard_B2ms jumpbox -``` - ---- - -## 8. Paso a Paso: Desplegar ContentFlow en Modo AILZ - -### 8.1 Obtener IDs de Recursos de la Mini-AILZ - -Usa el script incluido en ContentFlow o hazlo manualmente: - -#### Opción A: Script Automático (Recomendada) - -```bash -cd infra/scripts - -# Ejecutar con auto-set (configura variables de azd automáticamente) -./get-ailz-resources.sh --auto-set -# Cuando pregunte por el RG, ingresa: rg-mini-ailz -``` - -El script busca automáticamente: -- VNet y subredes (`pe-subnet`, `aca-env-subnet`) -- Las 6 Private DNS Zones requeridas -- JumpBox VM (busca tag `role=jumpbox` o nombre con `jmp`) -- Log Analytics y App Insights (opcionales) - -Genera un archivo `ailz-resources.env` y con `--auto-set` configura las variables en azd directamente. - -#### Opción B: Recopilación Manual - -```bash -# Obtener VNet Resource ID -VNET_ID=$(az network vnet show -g $AILZ_RG -n $VNET_NAME --query id -o tsv) -echo "VNET_ID: $VNET_ID" - -# Obtener IDs de Private DNS Zones -BLOB_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.blob.core.windows.net" --query id -o tsv) -COSMOS_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.documents.azure.com" --query id -o tsv) -APPCONFIG_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azconfig.io" --query id -o tsv) -ACR_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azurecr.io" --query id -o tsv) -COGNITIVE_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.cognitiveservices.azure.com" --query id -o tsv) -ACA_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azurecontainerapps.io" --query id -o tsv) - -# Opcionales -LOG_WS_ID=$(az monitor log-analytics workspace show -g $AILZ_RG -n "log-ailz-shared" --query id -o tsv 2>/dev/null || echo "") -APPI_ID=$(az monitor app-insights component show -g $AILZ_RG --app "appi-ailz-shared" --query id -o tsv 2>/dev/null || echo "") -``` - -### 8.2 Inicializar Entorno de azd - -```bash -# Ir al raíz del repositorio -cd /ruta/a/contentflow - -# Inicializar nuevo entorno azd -azd init -e contentflow-ailz-demo - -# Configurar ubicación y suscripción -azd env set AZURE_LOCATION "$LOCATION" -azd env set AZURE_AI_FOUNDRY_LOCATION "$LOCATION" -``` - -### 8.3 Configurar Variables de Modo AILZ - -```bash -# === MODO DE DESPLIEGUE === -azd env set DEPLOYMENT_MODE "ailz-integrated" - -# === RED === -azd env set EXISTING_VNET_RESOURCE_ID "$VNET_ID" -azd env set PRIVATE_ENDPOINT_SUBNET_NAME "pe-subnet" -azd env set CONTAINER_APPS_SUBNET_NAME "aca-env-subnet" - -# === PRIVATE DNS ZONES (6 requeridas) === -azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID "$BLOB_DNS_ID" -azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID "$COSMOS_DNS_ID" -azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID "$APPCONFIG_DNS_ID" -azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID "$ACR_DNS_ID" -azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID "$COGNITIVE_DNS_ID" -azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$ACA_DNS_ID" - -# === OPCIONALES: OBSERVABILIDAD COMPARTIDA === -# Solo si creaste Log Analytics y App Insights compartidos en el paso 7.7 -if [ ! -z "$LOG_WS_ID" ]; then - azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$LOG_WS_ID" -fi -if [ ! -z "$APPI_ID" ]; then - azd env set EXISTING_APP_INSIGHTS_ID "$APPI_ID" -fi - -# === Principal ID (tu usuario para RBAC) === -PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv) -azd env set AZURE_PRINCIPAL_ID "$PRINCIPAL_ID" -``` - -### 8.4 Verificar Configuración - -```bash -# Listar todas las variables del entorno azd -azd env get-values | grep -E "DEPLOYMENT_MODE|EXISTING_|PRIVATE_|CONTAINER_APPS_SUBNET|AZURE_LOCATION" -``` - -Salida esperada: -``` -DEPLOYMENT_MODE="ailz-integrated" -EXISTING_VNET_RESOURCE_ID="/subscriptions/.../virtualNetworks/vnet-ailz-demo" -PRIVATE_ENDPOINT_SUBNET_NAME="pe-subnet" -CONTAINER_APPS_SUBNET_NAME="aca-env-subnet" -EXISTING_BLOB_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.blob.core.windows.net" -EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.documents.azure.com" -EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azconfig.io" -EXISTING_ACR_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azurecr.io" -EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.cognitiveservices.azure.com" -EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azurecontainerapps.io" -AZURE_LOCATION="eastus2" -``` - -### 8.5 Desplegar - -```bash -# Despliegue completo (provision + build + deploy) -azd up -``` - -**¿Qué sucede durante `azd up`?** - -1. **Pre-provision hook**: Valida que Azure CLI, azd y Docker estén instalados -2. **Bicep Provisioning**: - - Valida parámetros AILZ (all 6 DNS zones + VNet + subredes) - - Crea User Assigned Identity - - Crea Storage Account con PE en `pe-subnet` → registra en `privatelink.blob.core.windows.net` - - Crea Cosmos DB con PE en `pe-subnet` → registra en `privatelink.documents.azure.com` - - Crea App Configuration con PE en `pe-subnet` → registra en `privatelink.azconfig.io` - - Crea Container Registry **Premium** con PE en `pe-subnet` → registra en `privatelink.azurecr.io` - - Crea Container Apps Environment **interno** en `aca-env-subnet` - - Crea Container Apps (API, Worker, Web) con ingress **interno** - - Crea AI Foundry Hub & Project - - Si no proporcionaste Log Analytics/App Insights existentes: crea nuevos - - Configura RBAC para la Managed Identity -3. **Container Build**: Construye imágenes Docker para API, Worker, Web -4. **Push to ACR**: Sube imágenes a Container Registry (vía PE si estás en la VNet, o vía Azure CLI si públicamente) -5. **Deploy to Container Apps**: Despliega las apps (internamente accesibles) -6. **Post-deploy hook**: Muestra endpoints (internos) - -> **⚠️ IMPORTANTE sobre el push a ACR:** En modo AILZ, el ACR tiene `publicNetworkAccess: Disabled`. Si ejecutas `azd up` desde tu máquina local (fuera de la VNet), el push de imágenes **podría fallar**. Ver sección de Troubleshooting para soluciones. - -### 8.6 Verificar Despliegue - -```bash -# Verificar que los Private Endpoints se crearon correctamente -az network private-endpoint list \ - --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) \ - --query "[].{name:name, status:privateLinkServiceConnections[0].privateLinkServiceConnectionState.status}" \ - -o table -``` - -Salida esperada: -``` -Name Status ------------------------ --------- -stXXXXX-blob-pe Approved -stXXXXX-queue-pe Approved -cosmos-XXXXX-pe Approved -appcs-XXXXX-pe Approved -crXXXXX-pe Approved -``` - ---- - -## 9. Acceso y Pruebas de Conectividad Privada - -### 9.1 Acceder vía JumpBox - -Dado que todos los servicios son internos, necesitas estar dentro de la VNet: - -```bash -# Conectar a la JumpBox vía Bastion (Portal Azure) -# O vía Azure CLI: -az network bastion ssh \ - --resource-group $AILZ_RG \ - --name "bastion-demo" \ - --target-resource-id $(az vm show -g $AILZ_RG -n "jmp-ailz-demo" --query id -o tsv) \ - --auth-type password \ - --username azureuser -``` - -### 9.2 Conseguir URLs Internas - -Las URLs de Container Apps en modo interno tienen el formato: -``` -https://...azurecontainerapps.io -``` - -Pero solo son resolvibles dentro de la VNet (gracias a la Private DNS Zone `privatelink.azurecontainerapps.io`). - -```bash -# Obtener URLs internas desde azd -azd env get-values | grep "SERVICE_.*_URI" - -# O directamente: -API_URL=$(az containerapp show -g $(azd env get-value AZURE_RESOURCE_GROUP) -n $(azd env get-value API_CONTAINER_APP_NAME) --query "properties.configuration.ingress.fqdn" -o tsv) -WEB_URL=$(az containerapp show -g $(azd env get-value AZURE_RESOURCE_GROUP) -n $(azd env get-value WEB_CONTAINER_APP_NAME) --query "properties.configuration.ingress.fqdn" -o tsv) - -echo "API: https://$API_URL" -echo "Web: https://$WEB_URL" -``` - -### 9.3 Probar desde la JumpBox - -Desde la JumpBox (Windows): -```powershell -# Verificar resolución DNS (debe resolver a IP privada 10.x.x.x) -nslookup - -# Probar API health check -Invoke-WebRequest -Uri "https:///health" -UseBasicParsing - -# Abrir navegador para la Web UI -Start-Process "https://" -``` - -Desde la JumpBox (Linux): -```bash -# Verificar resolución DNS -nslookup -# Debe retornar IP privada (10.x.x.x), NO una IP pública - -# Probar API -curl -k https:///health - -# Probar que los Private Endpoints resuelven correctamente -nslookup .blob.core.windows.net -# Debe retornar privatelink.blob.core.windows.net → 10.x.x.x - -nslookup .documents.azure.com -# Debe retornar privatelink.documents.azure.com → 10.x.x.x -``` - -### 9.4 Probar Resolución DNS de Cada Servicio - -```bash -# Desde la JumpBox, verificar que todos los PEs resuelven a IPs privadas -SERVICES=( - ".blob.core.windows.net" - ".queue.core.windows.net" - ".documents.azure.com" - ".azconfig.io" - ".azurecr.io" -) - -for SVC in "${SERVICES[@]}"; do - echo "=== $SVC ===" - nslookup $SVC - echo "" -done -``` - ---- - -## 10. Mapa Completo de Parámetros - -### Variables de Entorno azd → Parámetros Bicep - -| Variable azd | Parámetro Bicep | Requerido en AILZ | Default | Descripción | -|---|---|---|---|---| -| `DEPLOYMENT_MODE` | `deploymentMode` | ✅ | — | Debe ser `ailz-integrated` | -| `AZURE_ENV_NAME` | `environmentName` | ✅ | — | Nombre del entorno | -| `AZURE_LOCATION` | `location` | ✅ | — | Región Azure | -| `AZURE_AI_FOUNDRY_LOCATION` | `foundryLocation` | ✅ | — | Región para AI Foundry | -| `AZURE_PRINCIPAL_ID` | `principalId` | ⬜ | `""` | Tu Object ID para RBAC | -| `EXISTING_VNET_RESOURCE_ID` | `existingVnetResourceId` | ✅ | `""` | Resource ID completo de la VNet | -| `PRIVATE_ENDPOINT_SUBNET_NAME` | `privateEndpointSubnetName` | ✅ | `pe-subnet` | Nombre de la subred para PEs | -| `CONTAINER_APPS_SUBNET_NAME` | `containerAppsSubnetName` | ✅ | `aca-env-subnet` | Nombre de la subred para CAE | -| `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | `existingCognitiveServicesPrivateDnsZoneId` | ✅ | `""` | DNS Zone para AI Services | -| `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | `existingBlobPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Blob Storage | -| `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | `existingCosmosPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Cosmos DB | -| `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | `existingAppConfigPrivateDnsZoneId` | ✅ | `""` | DNS Zone para App Config | -| `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | `existingAcrPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Container Registry | -| `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | `existingContainerAppsEnvPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Container Apps | -| `EXISTING_KEY_VAULT_PRIVATE_DNS_ZONE_ID` | `existingKeyVaultPrivateDnsZoneId` | ⬜ | `""` | DNS Zone para Key Vault | -| `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | `existingLogAnalyticsWorkspaceId` | ⬜ | `""` | Log Analytics compartido | -| `EXISTING_APP_INSIGHTS_ID` | `existingAppInsightsId` | ⬜ | `""` | App Insights compartido | - -### Formato de Resource IDs - -``` -VNet: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{name} -DNS Zone: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/{zone-name} -Log Analyt: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{name} -App Insigh: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Insights/components/{name} -``` - ---- - -## 11. Cambios por Recurso en Modo AILZ - -### Comparativa Detallada - -| Recurso | Basic | AILZ-Integrated | -|---|---|---| -| **Storage Account** | SKU: Standard_LRS, Public | SKU: Standard_LRS, **PE (blob+queue)**, `Deny` ACL | -| **Cosmos DB** | Serverless, Public | Serverless, **PE**, `Disabled` public access | -| **App Configuration** | Standard, Public | Standard, **PE**, `Disabled` public access | -| **Container Registry** | **Standard** | **Premium** (requerido para PE), **PE** | -| **Container Apps Env** | Public, External LB | **VNet-integrated**, **Internal LB**, `aca-env-subnet` | -| **Container Apps (API)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | -| **Container Apps (Worker)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | -| **Container Apps (Web)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | -| **AI Foundry** | Public | **PE** para Cognitive Services | -| **Log Analytics** | Creado nuevo (basic) | Usa existente O crea nuevo | -| **App Insights** | Creado nuevo (basic) | Usa existente O crea nuevo | -| **Managed Identity** | Sin cambios | Sin cambios | - -### Nomenclatura de Private Endpoints - -ContentFlow usa una convención consistente: - -``` -{resourceName}-{service}-pe → Nombre del Private Endpoint -{resourceName}-{service}-plsc → Private Link Service Connection -{service}-dns-zone-group → DNS Zone Group -{service}-config → DNS Zone Group Config -``` - -Ejemplos: -- `st4a7b2c-blob-pe` / `st4a7b2c-blob-plsc` -- `st4a7b2c-queue-pe` / `st4a7b2c-queue-plsc` -- `cosmos-4a7b2c-pe` / `cosmos-4a7b2c-cosmos-plsc` -- `appcs-4a7b2c-pe` / `appcs-4a7b2c-app-config-plsc` -- `cr4a7b2c-pe` / `cr4a7b2c-acr-plsc` - ---- - -## 12. Troubleshooting - -### Problema 1: Error "ACR push failed" durante `azd up` - -**Causa:** ACR tiene `publicNetworkAccess: Disabled`. Tu máquina local no puede hacer push de imágenes. - -**Soluciones:** - -**Solución A - Temporalmente habilitar acceso público al ACR:** -```bash -# Antes de azd deploy -ACR_NAME=$(azd env get-value AZURE_CONTAINER_REGISTRY_NAME) -CF_RG=$(azd env get-value AZURE_RESOURCE_GROUP) - -# Habilitar temporalmente -az acr update -n $ACR_NAME -g $CF_RG --public-network-enabled true - -# Hacer el deploy -azd deploy - -# Volver a deshabilitar -az acr update -n $ACR_NAME -g $CF_RG --public-network-enabled false -``` - -**Solución B - Build y push desde la JumpBox:** -```bash -# Ejecutar azd up desde la JumpBox (dentro de la VNet) -# Requiere instalar azd, az cli, docker en la JumpBox -``` - -**Solución C - Usar ACR Tasks (build en la nube):** -```bash -# ACR Tasks puede hacer build sin necesidad de Docker local -az acr build --registry $ACR_NAME -t contentflow-api:latest ./contentflow-api/ -az acr build --registry $ACR_NAME -t contentflow-worker:latest ./contentflow-worker/ -az acr build --registry $ACR_NAME -t contentflow-web:latest ./contentflow-web/ -``` - -### Problema 2: Error "VNet subnet not found" - -**Causa:** Los nombres de subred no coinciden con los esperados. - -**Solución:** -```bash -# Verificar nombres exactos -az network vnet subnet list -g $AILZ_RG --vnet-name $VNET_NAME --query "[].name" -o tsv - -# Si los nombres son diferentes, ajustar: -azd env set PRIVATE_ENDPOINT_SUBNET_NAME "tu-nombre-de-pe-subnet" -azd env set CONTAINER_APPS_SUBNET_NAME "tu-nombre-de-aca-subnet" -``` - -### Problema 3: Error de validación "fail('existingXXX is required')" - -**Causa:** Falta algún parámetro requerido para modo `ailz-integrated`. - -**Solución:** Ejecutar el script de verificación: -```bash -# Verificar todas las variables están configuradas -REQUIRED_VARS=( - "DEPLOYMENT_MODE" - "EXISTING_VNET_RESOURCE_ID" - "EXISTING_BLOB_PRIVATE_DNS_ZONE_ID" - "EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID" - "EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID" - "EXISTING_ACR_PRIVATE_DNS_ZONE_ID" - "EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID" - "EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID" -) - -for VAR in "${REQUIRED_VARS[@]}"; do - VALUE=$(azd env get-value $VAR 2>/dev/null) - if [ -z "$VALUE" ]; then - echo "❌ FALTA: $VAR" - else - echo "✓ $VAR configurada" - fi -done -``` - -### Problema 4: DNS no resuelve a IP privada desde JumpBox - -**Causa:** La Private DNS Zone no está vinculada a la VNet, o el DNS Zone Group no se creó. - -**Solución:** -```bash -# Verificar que cada zona tiene un VNet link -for ZONE in "privatelink.blob.core.windows.net" "privatelink.documents.azure.com" "privatelink.azconfig.io" "privatelink.azurecr.io" "privatelink.cognitiveservices.azure.com" "privatelink.azurecontainerapps.io"; do - echo "=== $ZONE ===" - az network private-dns link vnet list --zone-name $ZONE -g $AILZ_RG --query "[].{name:name, state:virtualNetworkLinkState}" -o table -done - -# Si falta un link: -az network private-dns link vnet create \ - --resource-group $AILZ_RG \ - --zone-name "privatelink.blob.core.windows.net" \ - --name "blob-vnet-link" \ - --virtual-network "$VNET_ID" \ - --registration-enabled false -``` - -### Problema 5: Container Apps no inician (health check failed) - -**Causa:** Las Container Apps no pueden alcanzar los servicios backend (Storage, Cosmos) porque los PEs no están configurados correctamente o los DNS Zone Groups no registraron los records A. - -**Solución:** -```bash -# Verificar logs de Container Apps -az containerapp logs show \ - -n $(azd env get-value API_CONTAINER_APP_NAME) \ - -g $(azd env get-value AZURE_RESOURCE_GROUP) \ - --type system - -# Verificar que los A records existen en las DNS zones -az network private-dns record-set a list -g $AILZ_RG -z "privatelink.blob.core.windows.net" -o table -az network private-dns record-set a list -g $AILZ_RG -z "privatelink.documents.azure.com" -o table -``` - -### Problema 6: Permisos insuficientes para crear PE en subnet de otro RG - -**Causa:** Tu usuario no tiene `Network Contributor` en el RG de la VNet. - -**Solución:** -```bash -# Asignar permisos en el RG de red -USER_OBJ_ID=$(az ad signed-in-user show --query id -o tsv) - -az role assignment create \ - --assignee $USER_OBJ_ID \ - --role "Network Contributor" \ - --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG" - -az role assignment create \ - --assignee $USER_OBJ_ID \ - --role "Private DNS Zone Contributor" \ - --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG" -``` - -### Problema 7: Error "Container Apps Environment subnet too small" - -**Causa:** La subred `aca-env-subnet` es menor a `/23`. - -**Solución:** Recrear la subred con el tamaño correcto (requiere borrar la existente primero si no tiene recursos): -```bash -az network vnet subnet delete -g $AILZ_RG --vnet-name $VNET_NAME -n $ACA_SUBNET_NAME -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name $ACA_SUBNET_NAME \ - --address-prefix "10.0.16.0/23" -``` - ---- - -## 13. Costos Estimados de la Mini-AILZ - -### Recursos de Red (RG: rg-mini-ailz) - -| Recurso | SKU/Tier | Costo Estimado (USD/mes) | -|---|---|---| -| VNet + Subredes | Gratuito | $0 | -| Private DNS Zones (6) | $0.50/zona | ~$3 | -| Azure Bastion | Developer SKU | ~$5.50 | -| JumpBox VM (B2ms, Windows) | Standard_B2ms | ~$60 (24/7) | -| JumpBox VM (B2s, Linux) | Standard_B2s | ~$15 (24/7) | - -**Tip para reducir costos de demo:** -```bash -# Apagar la JumpBox cuando no la uses -az vm deallocate -g $AILZ_RG -n "jmp-ailz-demo" - -# Encenderla cuando la necesites -az vm start -g $AILZ_RG -n "jmp-ailz-demo" -``` - -### Recursos ContentFlow Adicionales en AILZ vs Basic - -| Recurso | Cambio en AILZ | Impacto en Costo | -|---|---|---| -| Container Registry | Standard → **Premium** | +~$45/mes | -| Private Endpoints (5-6) | Cada PE tiene costo | +~$5/mes total | -| Procesamiento de PE (datos) | Por GB procesado | Mínimo en demo | - -### Costo Total Estimado de Demo (Mini-AILZ + ContentFlow) - -| Componente | Costo Estimado | -|---|---| -| Mini-AILZ (red + JumpBox Linux + Bastion) | ~$24/mes | -| ContentFlow AILZ (Premium ACR, PEs) delta vs Basic | ~$50/mes | -| ContentFlow base (Storage, Cosmos, Container Apps, AI) | ~$30-80/mes | -| **Total estimado** | **~$100-150/mes** | - -> **Nota:** Los costos de AI Foundry (GPT-4.1) son por uso (tokens). El estimado base asume uso mínimo. Apaga la JumpBox cuando no la uses para reducir costos. - ---- - -## 14. Limpieza de Recursos - -### Eliminar ContentFlow - -```bash -# Eliminar todos los recursos de ContentFlow -azd down --force --purge -``` - -> `--purge` elimina los recursos en soft-delete (Cosmos DB, Key Vault, AI Services). - -### Eliminar la Mini-AILZ - -```bash -# Eliminar todo el Resource Group de red -az group delete --name $AILZ_RG --yes --no-wait -``` - -### Limpieza Selectiva (Mantener la Red) - -Si quieres mantener la Mini-AILZ para futuras demos: -```bash -# Solo eliminar ContentFlow -azd down --force --purge - -# Apagar JumpBox para ahorrar -az vm deallocate -g $AILZ_RG -n "jmp-ailz-demo" -``` - ---- - -## Apéndice A: Script Completo de Creación de Mini-AILZ - -Para conveniencia, aquí está el script completo que crea toda la Mini-AILZ de un solo paso: - -```bash -#!/bin/bash -# create-mini-ailz.sh - Crea una AI Landing Zone mínima para demo de ContentFlow -set -e - -# === CONFIGURACIÓN (AJUSTAR SEGÚN TU AMBIENTE) === -SUBSCRIPTION_ID=$(az account show --query id -o tsv) -LOCATION="eastus2" -AILZ_RG="rg-mini-ailz" -VNET_NAME="vnet-ailz-demo" -VNET_PREFIX="10.0.0.0/16" -PE_SUBNET_PREFIX="10.0.1.0/27" -ACA_SUBNET_PREFIX="10.0.16.0/23" -JUMPBOX_SUBNET_PREFIX="10.0.2.0/27" -BASTION_SUBNET_PREFIX="10.0.3.0/26" - -echo "=== Creando Mini-AILZ para ContentFlow Demo ===" -echo "Suscripción: $SUBSCRIPTION_ID" -echo "Ubicación: $LOCATION" -echo "Resource Group: $AILZ_RG" -echo "" - -# 1. Resource Group -echo "[1/6] Creando Resource Group..." -az group create -n $AILZ_RG -l $LOCATION --tags purpose=mini-ailz -o none - -# 2. VNet + Subredes -echo "[2/6] Creando VNet y subredes..." -az network vnet create -g $AILZ_RG -n $VNET_NAME --address-prefix $VNET_PREFIX -l $LOCATION -o none -az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "pe-subnet" --address-prefix $PE_SUBNET_PREFIX -o none -az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "aca-env-subnet" --address-prefix $ACA_SUBNET_PREFIX -o none -az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "jumpbox-subnet" --address-prefix $JUMPBOX_SUBNET_PREFIX -o none -az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "AzureBastionSubnet" --address-prefix $BASTION_SUBNET_PREFIX -o none -echo " ✓ VNet con 4 subredes creada" - -# 3. Private DNS Zones -echo "[3/6] Creando Private DNS Zones..." -VNET_ID="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" -DNS_ZONES=( - "privatelink.blob.core.windows.net" - "privatelink.documents.azure.com" - "privatelink.azconfig.io" - "privatelink.azurecr.io" - "privatelink.cognitiveservices.azure.com" - "privatelink.azurecontainerapps.io" -) -for ZONE in "${DNS_ZONES[@]}"; do - az network private-dns zone create -g $AILZ_RG -n "$ZONE" -o none - LINK_NAME=$(echo "$ZONE" | sed 's/\./-/g')-link - az network private-dns link vnet create -g $AILZ_RG -z "$ZONE" -n "$LINK_NAME" -v "$VNET_ID" -e false -o none -done -echo " ✓ 6 Private DNS Zones creadas y vinculadas" - -# 4. Azure Bastion -echo "[4/6] Creando Azure Bastion..." -az network public-ip create -g $AILZ_RG -n "pip-bastion-demo" --sku Standard --allocation-method Static -l $LOCATION -o none -az network bastion create -g $AILZ_RG -n "bastion-demo" --public-ip-address "pip-bastion-demo" --vnet-name $VNET_NAME --sku Developer -l $LOCATION -o none -echo " ✓ Azure Bastion (Developer SKU) creado" - -# 5. JumpBox VM (Linux para costo mínimo) -echo "[5/6] Creando JumpBox VM..." -az vm create -g $AILZ_RG -n "jmp-ailz-demo" \ - --image "Ubuntu2404" --size "Standard_B2s" \ - --vnet-name $VNET_NAME --subnet "jumpbox-subnet" \ - --admin-username "azureuser" --generate-ssh-keys \ - --tags role=jumpbox purpose=mini-ailz \ - --public-ip-address "" -l $LOCATION -o none -echo " ✓ JumpBox Linux creada" - -# 6. Resumen -echo "" -echo "[6/6] Verificación..." -echo "" -echo "=== Mini-AILZ Creada Exitosamente ===" -echo "" -echo "Resource Group: $AILZ_RG" -echo "VNet: $VNET_NAME ($VNET_PREFIX)" -echo "Subredes:" -echo " - pe-subnet: $PE_SUBNET_PREFIX" -echo " - aca-env-subnet: $ACA_SUBNET_PREFIX" -echo " - jumpbox-subnet: $JUMPBOX_SUBNET_PREFIX" -echo " - AzureBastionSubnet: $BASTION_SUBNET_PREFIX" -echo "" -echo "Private DNS Zones: 6 zonas creadas y vinculadas" -echo "JumpBox: jmp-ailz-demo (acceso vía Bastion)" -echo "" -echo "=== Siguiente paso ===" -echo "Ejecuta el script de descubrimiento de ContentFlow:" -echo " cd infra/scripts && ./get-ailz-resources.sh --auto-set" -echo "Luego despliega:" -echo " azd env set DEPLOYMENT_MODE ailz-integrated" -echo " azd up" -``` - ---- - -## Apéndice B: Diagrama de Flujo de Despliegue - -``` -┌────────────────────────┐ -│ 1. Preparar Mini-AILZ │ -│ (create-mini-ailz.sh) │ -└────────┬───────────────┘ - │ - ▼ -┌────────────────────────┐ -│ 2. Obtener IDs │ -│ (get-ailz-resources │ -│ --auto-set) │ -└────────┬───────────────┘ - │ - ▼ -┌────────────────────────┐ -│ 3. Configurar azd │ -│ DEPLOYMENT_MODE= │ -│ ailz-integrated │ -└────────┬───────────────┘ - │ - ▼ -┌────────────────────────┐ -│ 4. azd up │ -│ (provision + deploy) │ -└────────┬───────────────┘ - │ - ▼ -┌────────────────────────┐ ┌──────────────────────┐ -│ 5. Conectar a │────▶│ 6. Probar servicios │ -│ JumpBox vía Bastion │ │ internos (API, Web) │ -└────────────────────────┘ └──────────────────────┘ -``` - ---- - -> **Última actualización:** Febrero 2026 -> **Aplica a:** ContentFlow con plantillas Bicep para despliegue `basic` y `ailz-integrated` diff --git a/Analysis/05-mini-ailz.md b/Analysis/05-mini-ailz.md deleted file mode 100644 index f7e6714..0000000 --- a/Analysis/05-mini-ailz.md +++ /dev/null @@ -1,1002 +0,0 @@ -# Mini-AILZ con Terraform AVM Pattern Module para ContentFlow - -> **Documento técnico operativo** — Despliegue de una AI Landing Zone mínima usando el módulo oficial Terraform AVM `Azure/avm-ptn-aiml-landing-zone`, optimizada para el costo mínimo necesario para ContentFlow en modo `ailz-integrated`. - ---- - -## Tabla de Contenidos - -1. [Objetivo y Contexto](#1-objetivo-y-contexto) -2. [¿Por qué el Pattern Module en lugar de Azure CLI Manual?](#2-por-qué-el-pattern-module-en-lugar-de-azure-cli-manual) -3. [Análisis del Módulo: Todos los Recursos vs Lo Mínimo](#3-análisis-del-módulo-todos-los-recursos-vs-lo-mínimo) -4. [Mapeo: Requisitos ContentFlow → Parámetros del Módulo](#4-mapeo-requisitos-contentflow--parámetros-del-módulo) -5. [Configuración Terraform Mínima Completa](#5-configuración-terraform-mínima-completa) -6. [Conexión de Outputs con ContentFlow azd](#6-conexión-de-outputs-con-contentflow-azd) -7. [Fix Conocido: storage_use_azuread](#7-fix-conocido-storage_use_azuread) -8. [Paso a Paso: Despliegue](#8-paso-a-paso-despliegue) -9. [Comparación de Costo: Full vs Mini](#9-comparación-de-costo-full-vs-mini) -10. [Diagrama de Arquitectura Resultante](#10-diagrama-de-arquitectura-resultante) -11. [Notas de Operación y Troubleshooting](#11-notas-de-operación-y-troubleshooting) -12. [Limpieza de Recursos](#12-limpieza-de-recursos) - ---- - -## 1. Objetivo y Contexto - -### Situación - -ContentFlow soporta despliegue en modo `ailz-integrated`, el cual requiere infraestructura de red pre-existente (VNet, subredes, Private DNS Zones, Container Apps Environment). En el [documento 04](04-ai_lz_options.md) se describe cómo crear esta infraestructura con comandos Azure CLI manuales. - -### Nuevo Enfoque - -En lugar de scripts manuales, usaremos el **módulo oficial Terraform AVM Pattern** para AI/ML Landing Zones: - -- **Módulo**: [`Azure/avm-ptn-aiml-landing-zone/azurerm`](https://github.com/Azure/terraform-azurerm-avm-ptn-aiml-landing-zone) -- **Versión**: `0.4.0` -- **Registry**: [Terraform Registry](https://registry.terraform.io/modules/Azure/avm-ptn-aiml-landing-zone/azurerm/latest) - -### Principio Clave: Solo lo Mínimo Indispensable - -El módulo completo despliega **más de 25 recursos** incluyendo AI Foundry, App Gateway, Firewall, APIM, Build VM, AI Search, Bing Grounding, etc. ContentFlow **no necesita la mayoría de estos**. Este documento configura **exclusivamente** lo que ContentFlow requiere para funcionar en modo privado. - ---- - -## 2. ¿Por qué el Pattern Module en lugar de Azure CLI Manual? - -| Aspecto | Azure CLI Manual (Doc 04) | Terraform AVM Module (Este Doc) | -|---|---|---| -| **Reproducibilidad** | Scripts bash con variables | Declarativo, idempotente, plan/apply | -| **Estado** | No hay tracking de estado | `terraform.tfstate` con full tracking | -| **Drift detection** | Manual (`az resource show`) | `terraform plan` detecta drift | -| **Limpieza** | `az group delete` (todo o nada) | `terraform destroy` selectivo | -| **Validación** | Post-ejecución manual | `terraform validate` + `plan` pre-apply | -| **Subredes y DNS** | Creación manual una por una | El módulo crea subredes automáticamente | -| **Private DNS Zones** | 6 comandos individuales + links | El módulo las crea y vincula | -| **Container Apps Env** | No incluido en doc 04 | Incluido con VNet integration | -| **Bastion** | Configuración manual compleja | Un flag `deploy = true` | -| **Versionamiento** | Copiar/pegar scripts | Module version pinning | -| **Soporte oficial** | Ninguno | Módulo oficial de Microsoft (AVM) | - -**Ventaja principal**: El módulo maneja internamente la creación de subredes con los nombres, tamaños y delegaciones correctas. Esto elimina errores comunes como subredes con prefijos incorrectos o sin delegación para Container Apps. - ---- - -## 3. Análisis del Módulo: Todos los Recursos vs Lo Mínimo - -### Recursos que el módulo PUEDE desplegar (despliegue completo) - -| Recurso | Default `deploy` | ¿ContentFlow lo necesita? | Acción Mini-AILZ | -|---|---|---|---| -| **VNet + Subredes** | Siempre (required) | **SÍ** | ✅ Mantener | -| **Private DNS Zones** | Siempre | **SÍ** | ✅ Mantener | -| **Log Analytics Workspace** | `true` | **SÍ** (compartido) | ✅ Mantener | -| **NSGs** | Siempre | **SÍ** (seguridad) | ✅ Mantener | -| **Container Apps Environment** | `true` | **SÍ** | ✅ Mantener | -| **Bastion** | `true` | **SÍ** (acceso a red privada) | ✅ Mantener | -| **GenAI Container Registry** | `true` | **Parcial** (ContentFlow crea el suyo) | ⚠️ Mantener* | -| **GenAI Key Vault** | Siempre | **Parcial** | ⚠️ Mantener* | -| **GenAI Storage Account** | `true` | **No directamente** | ⚠️ Mantener* | -| **GenAI App Configuration** | `true` | **No directamente** | ⚠️ Mantener* | -| **GenAI Cosmos DB** | `true` | **No directamente** | ⚠️ Mantener* | -| **AI Foundry Hub + BYOR** | `{}` (vacío = no crea) | **No** | ❌ Omitir | -| **AI Model Deployments** | `{}` (vacío = no crea) | **No** | ❌ Omitir | -| **AI Projects** | `{}` (vacío = no crea) | **No** | ❌ Omitir | -| **App Gateway** | `null` (no crea) | **No** | ❌ Omitir | -| **Azure Firewall** | `true` | **No** (demo) | ❌ Deshabilitar | -| **Build VM / Jump VM** | `true` | **No** (usamos Bastion) | ❌ Deshabilitar | -| **AI Search (BYOR)** | `{}` (vacío = no crea) | **No** | ❌ Omitir | -| **AI Search (KS)** | `true` | **No** | ❌ Deshabilitar | -| **Bing Grounding** | `true` | **No** | ❌ Deshabilitar | -| **APIM** | `true` | **No** | ❌ Deshabilitar | -| **WAF Policy** | Solo con App GW | **No** | ❌ Omitir | - -> **\*** Los recursos GenAI (Container Registry, Key Vault, Storage, App Config, Cosmos) se crean por defecto como parte de la plataforma GenAI del módulo. ContentFlow crea sus propios recursos de datos (Cosmos, Storage, App Config) durante `azd up`, pero el módulo necesita el Container Registry y Key Vault internamente. Para minimizar costo, podemos aceptar estos con configuración por defecto ligera. - -### ¿Qué NO se puede deshabilitar? - -El módulo **siempre crea** estos recursos independientemente de la configuración: - -1. **Resource Group** (si no existe) -2. **VNet** (required input) -3. **Subredes** (calculadas internamente según recursos habilitados) -4. **Private DNS Zones** (necesarias para private endpoints) -5. **NSGs** (asociados a subredes) -6. **Key Vault** (usado internamente por el módulo para secretos de GenAI) -7. **Role Assignments** (deployment user → Key Vault Admin) - ---- - -## 4. Mapeo: Requisitos ContentFlow → Parámetros del Módulo - -### Lo que ContentFlow Necesita del AILZ - -| Requisito ContentFlow | Parámetro del Módulo | Variable de Entorno `azd` | -|---|---|---| -| VNet Resource ID | `module.ailz.virtual_network.id` | `EXISTING_VNET_RESOURCE_ID` | -| Subred `pe-subnet` | Creada automáticamente por el módulo | `PRIVATE_ENDPOINT_SUBNET_NAME` | -| Subred `aca-env-subnet` | Creada automáticamente por el módulo | `CONTAINER_APPS_SUBNET_NAME` | -| DNS: `privatelink.blob.core.windows.net` | Parte de `private_dns_zones` del módulo | `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.documents.azure.com` | Parte de `private_dns_zones` del módulo | `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.azconfig.io` | Parte de `private_dns_zones` del módulo | `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.azurecr.io` | Parte de `private_dns_zones` del módulo | `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.cognitiveservices.azure.com` | Parte de `private_dns_zones` del módulo | `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.azurecontainerapps.io` | Parte de `private_dns_zones` del módulo | `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | -| Log Analytics Workspace | `module.ailz.log_analytics_workspace_id` | `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | -| Container Apps Environment | Creado por el módulo (VNet-integrated, interno) | El CAE se crea aquí, ContentFlow lo usa | - -### Nota Importante sobre Subredes - -El módulo AVM Pattern calcula y crea las subredes internamente basándose en qué recursos están habilitados. Los nombres de subredes son determinados por el módulo. ContentFlow espera subredes llamadas `pe-subnet` y `aca-env-subnet`. Puedes usar el bloque `vnet_definition.subnets` para configurar override de nombres si es necesario. - -### Nota sobre Container Apps Environment - -El módulo crea un Container Apps Environment integrado con la VNet. Esto es **exactamente lo que ContentFlow necesita**. Sin embargo, ContentFlow normalmente crea su propio CAE durante `azd up`. Para el modo Mini-AILZ con Terraform, tienes dos opciones: - -1. **Usar el CAE del módulo** — Más eficiente; ContentFlow debe configurarse para no crear un CAE propio -2. **Deshabilitar el CAE del módulo** — ContentFlow crea el suyo durante deploy, usando la subred apropiada - -La **opción recomendada es la 1** (usar el CAE del módulo) ya que garantiza la configuración correcta de VNet integration. - ---- - -## 5. Configuración Terraform Mínima Completa - -### Estructura de Archivos - -``` -mini-ailz-contentflow/ -├── terraform.tf # Providers y versiones -├── variables.tf # Variables de entrada -├── main.tf # Data sources y helpers -├── ailz.tf # Módulo AILZ con configuración mínima -├── outputs.tf # Outputs para ContentFlow -└── terraform.tfvars # Valores (opcional, gitignored) -``` - -### `terraform.tf` — Providers - -```hcl -terraform { - required_version = ">= 1.9, < 2.0" - - required_providers { - azurerm = { - source = "hashicorp/azurerm" - version = ">= 3.116, < 5.0" - } - azapi = { - source = "Azure/azapi" - version = "~> 2.0" - } - random = { - source = "hashicorp/random" - version = "~> 3.5" - } - } -} - -provider "azurerm" { - features { - cognitive_account { - purge_soft_delete_on_destroy = true - } - key_vault { - purge_soft_delete_on_destroy = true - } - resource_group { - prevent_deletion_if_contains_resources = false - } - } - - # CRÍTICO: Sin esto, las Storage Accounts dan error 403 - # Ver sección 7 para detalles - storage_use_azuread = true -} - -provider "azapi" {} -``` - -### `variables.tf` — Variables de Entrada - -```hcl -variable "location" { - type = string - description = "Región de Azure para los recursos" - default = "swedencentral" -} - -variable "resource_group_name" { - type = string - description = "Nombre del Resource Group para la Mini-AILZ" - default = "rg-mini-ailz-contentflow" -} - -variable "name_prefix" { - type = string - description = "Prefijo para nombres de recursos (máx 10 chars, minúsculas alfanuméricas)" - default = "cfailz" - - validation { - condition = length(var.name_prefix) <= 10 && can(regex("^[a-z0-9]+$", var.name_prefix)) - error_message = "name_prefix debe tener máximo 10 caracteres alfanuméricos en minúsculas." - } -} - -variable "enable_telemetry" { - type = bool - description = "Habilitar telemetría del módulo AVM" - default = false -} -``` - -### `main.tf` — Data Sources - -```hcl -data "azurerm_client_config" "current" {} -``` - -### `ailz.tf` — Módulo AILZ Configuración Mínima - -```hcl -# ============================================================================= -# Mini-AILZ para ContentFlow -# ============================================================================= -# Este archivo configura SOLO lo mínimo indispensable del módulo AVM Pattern -# para satisfacer los requisitos de ContentFlow en modo ailz-integrated: -# 1. VNet con subredes (pe-subnet, aca-env-subnet) -# 2. Private DNS Zones vinculadas a la VNet -# 3. Container Apps Environment (VNet-integrated, internal LB) -# 4. Bastion (acceso a la red privada) -# 5. Log Analytics Workspace (monitoreo compartido) -# 6. NSGs (seguridad de subredes) -# 7. Key Vault + Container Registry (requeridos internamente por el módulo) -# -# TODO lo demás está DESHABILITADO para minimizar costo: -# ✗ AI Foundry, Model Deployments, AI Projects -# ✗ App Gateway, WAF Policy -# ✗ Azure Firewall -# ✗ Build VM, Jump VM -# ✗ AI Search (BYOR y KS) -# ✗ Bing Grounding -# ✗ APIM -# ============================================================================= - -module "ailz" { - source = "Azure/avm-ptn-aiml-landing-zone/azurerm" - version = "0.4.0" - - # ── Ubicación y Resource Group ────────────────────────────────────────────── - location = var.location - resource_group_name = var.resource_group_name - - # ── VNet (REQUIRED) ──────────────────────────────────────────────────────── - # ContentFlow necesita: - # - pe-subnet: para Private Endpoints (~32 IPs, /27+) - # - aca-env-subnet: para Container Apps Environment (~512 IPs, /23) - # - # NOTA: El módulo calcula y crea las subredes internamente. - # Usamos 10.0.0.0/16 para tener espacio suficiente. - vnet_definition = { - address_space = ["10.0.0.0/16"] - enable_diagnostic_settings = false - } - - # ── Container Apps Environment ───────────────────────────────────────────── - # ContentFlow despliega API, Worker y Web como Container Apps. - # Este CAE se integra con la VNet en modo interno (solo accesible desde VNet). - container_app_environment_definition = { - internal_load_balancer_enabled = true - zone_redundancy_enabled = false # false = ahorro de costo para demo - enable_diagnostic_settings = false - workload_profile = [ - { - name = "Consumption" - workload_profile_type = "Consumption" - } - ] - } - - # ── Bastion ──────────────────────────────────────────────────────────────── - # Necesario para acceder a la red privada (UI de ContentFlow, debug, etc.) - # SKU "Basic" es más barato que "Standard" (~$140/mes vs ~$350/mes) - bastion_definition = { - deploy = true - sku = "Basic" - zones = [] # Sin zone redundancy para demo - } - - # ── Log Analytics Workspace ──────────────────────────────────────────────── - # Compartido con ContentFlow para monitoreo centralizado. - law_definition = { - deploy = true - } - - # ── AI Foundry ───────────────────────────────────────────────────────────── - # ContentFlow NO necesita AI Foundry del AILZ. - # (ContentFlow crea su propio AI Foundry durante azd up) - # Dejamos el default vacío {} = no crea hub, projects, ni BYOR resources. - ai_foundry_definition = { - create_byor = false # No crear BYOR resources (AI Search, Cosmos, KV, Storage) - ai_foundry = { - enable_diagnostic_settings = false - } - } - - # ── DESHABILITADOS ───────────────────────────────────────────────────────── - - # App Gateway: ContentFlow no usa App Gateway (acceso via Bastion/VPN) - # Default es null = no se despliega - app_gateway_definition = null - - # Azure Firewall: No necesario para demo (ahorra ~$250/mes) - firewall_definition = { - deploy = false - } - - # Build VM: No necesaria (usamos Bastion + local development) - buildvm_definition = { - deploy = false - } - - # Jump VM: No necesaria si tenemos Bastion - # (jumpvm_definition no tiene deploy flag, se controla por buildvm) - - # KS AI Search: ContentFlow no necesita AI Search federado - ks_ai_search_definition = { - deploy = false - } - - # Bing Grounding: ContentFlow no usa Bing - ks_bing_grounding_definition = { - deploy = false - } - - # APIM: ContentFlow no usa API Management - apim_definition = { - deploy = false - publisher_email = "noreply@example.com" - publisher_name = "N/A" - } - - # ── GenAI Platform Resources ─────────────────────────────────────────────── - # Estos recursos se crean por defecto como parte de la plataforma GenAI. - # ContentFlow crea sus propios Cosmos DB, Storage, App Config durante azd up, - # pero el módulo internamente necesita algunos de estos. - # Configuración mínima para reducir costo: - - genai_container_registry_definition = { - zone_redundancy_enabled = false # Ahorro: sin ZRS - enable_diagnostic_settings = false - } - - genai_key_vault_definition = { - # Acceso público habilitado para facilitar deploy desde local - # En producción: false + Private Endpoint - public_network_access_enabled = true - network_acls = { - bypass = "AzureServices" - default_action = "Deny" - } - } - - genai_storage_account_definition = { - account_replication_type = "LRS" # LRS es más barato que GRS (default) - enable_diagnostic_settings = false - } - - genai_app_configuration_definition = { - purge_protection_enabled = false # Facilita destroy para demo - enable_diagnostic_settings = false - } - - genai_cosmosdb_definition = { - analytical_storage_enabled = false # No necesario para demo - automatic_failover_enabled = false # No necesario para single-region demo - enable_diagnostic_settings = false - } - - # ── Flags y otros ────────────────────────────────────────────────────────── - # flag_platform_landing_zone = false → No crea route tables con Firewall - # (Si es true, se asume que hay un Firewall y crea UDRs para enrutar tráfico) - flag_platform_landing_zone = false - - enable_telemetry = var.enable_telemetry - name_prefix = var.name_prefix -} -``` - -### `outputs.tf` — Valores para ContentFlow - -```hcl -# ============================================================================= -# Outputs para configurar ContentFlow azd environment -# ============================================================================= -# Estos valores se usan como variables de entorno para: -# azd env set -# antes de ejecutar: azd up --deployment-mode ailz-integrated -# ============================================================================= - -output "vnet_resource_id" { - description = "VNet Resource ID → EXISTING_VNET_RESOURCE_ID" - value = module.ailz.virtual_network.id -} - -output "log_analytics_workspace_id" { - description = "Log Analytics Workspace ID → EXISTING_LOG_ANALYTICS_WORKSPACE_ID" - value = module.ailz.log_analytics_workspace_id -} - -output "subnets" { - description = "Mapa de subredes creadas (para verificar nombres)" - value = module.ailz.subnets -} - -# ============================================================================= -# NOTA SOBRE PRIVATE DNS ZONES: -# ============================================================================= -# El módulo crea las Private DNS Zones automáticamente, pero actualmente (v0.4.0) -# NO expone sus Resource IDs como outputs directos. -# -# Para obtener los IDs de las DNS Zones, después de `terraform apply`: -# -# az network private-dns zone list \ -# --resource-group \ -# --query "[].{name:name, id:id}" -o table -# -# Alternativamente, puedes usar data sources de Terraform para leerlos: -# ============================================================================= - -# Instrucciones que se imprimen después del apply -output "contentflow_setup_instructions" { - description = "Instrucciones para configurar ContentFlow con esta Mini-AILZ" - value = <<-EOT - - ╔══════════════════════════════════════════════════════════════════╗ - ║ Mini-AILZ desplegada exitosamente para ContentFlow ║ - ╚══════════════════════════════════════════════════════════════════╝ - - Próximos pasos: - - 1. Obtener los Resource IDs de las Private DNS Zones: - - az network private-dns zone list \ - --resource-group ${var.resource_group_name} \ - --query "[].{name:name, id:id}" -o table - - 2. Configurar el environment de ContentFlow: - - cd - azd env set DEPLOYMENT_MODE ailz-integrated - azd env set EXISTING_VNET_RESOURCE_ID ${module.ailz.virtual_network.id} - azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID ${module.ailz.log_analytics_workspace_id} - - # Copiar los IDs de DNS Zones del paso 1: - azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID - - 3. Desplegar ContentFlow: - - azd up - - EOT -} -``` - ---- - -## 6. Conexión de Outputs con ContentFlow azd - -Después de ejecutar `terraform apply`, necesitas pasar los valores de infraestructura a ContentFlow. - -### Script Automatizado: `configure-contentflow.sh` - -```bash -#!/bin/bash -# ============================================================================= -# configure-contentflow.sh -# Configura las variables de entorno de ContentFlow azd con los outputs -# de la Mini-AILZ desplegada con Terraform. -# ============================================================================= -set -euo pipefail - -# Verificar que estamos en el directorio correcto de Terraform -if [[ ! -f "ailz.tf" ]]; then - echo "ERROR: Ejecuta este script desde el directorio de Terraform Mini-AILZ" - exit 1 -fi - -# Directorio de ContentFlow (ajustar según tu setup) -CONTENTFLOW_DIR="${1:-../contentflow-test-001}" - -echo "Obteniendo outputs de Terraform..." -VNET_ID=$(terraform output -raw vnet_resource_id) -LAW_ID=$(terraform output -raw log_analytics_workspace_id) -RG_NAME=$(terraform output -raw 2>/dev/null | grep -oP 'resource-group \K[^ \\]+' || true) - -# Obtener el nombre real del resource group del state -RG_NAME=$(terraform show -json | python3 -c " -import sys, json -state = json.load(sys.stdin) -for r in state.get('values', {}).get('root_module', {}).get('child_modules', []): - for res in r.get('resources', []): - if res['type'] == 'azurerm_resource_group': - print(res['values']['name']) - break -" 2>/dev/null || echo "rg-mini-ailz-contentflow") - -echo "Resource Group: $RG_NAME" -echo "VNet ID: $VNET_ID" -echo "Log Analytics ID: $LAW_ID" - -echo "" -echo "Obteniendo Private DNS Zone IDs..." - -# Función helper para obtener DNS Zone ID -get_dns_zone_id() { - local zone_name=$1 - az network private-dns zone show \ - --resource-group "$RG_NAME" \ - --name "$zone_name" \ - --query id -o tsv 2>/dev/null || echo "" -} - -BLOB_DNS=$(get_dns_zone_id "privatelink.blob.core.windows.net") -COSMOS_DNS=$(get_dns_zone_id "privatelink.documents.azure.com") -APPCONFIG_DNS=$(get_dns_zone_id "privatelink.azconfig.io") -ACR_DNS=$(get_dns_zone_id "privatelink.azurecr.io") -COGNITIVE_DNS=$(get_dns_zone_id "privatelink.cognitiveservices.azure.com") -CONTAINERAPP_DNS=$(get_dns_zone_id "privatelink.azurecontainerapps.io") - -# Verificar que encontramos todas las zonas -MISSING=0 -for ZONE_VAR in BLOB_DNS COSMOS_DNS APPCONFIG_DNS ACR_DNS COGNITIVE_DNS CONTAINERAPP_DNS; do - if [[ -z "${!ZONE_VAR}" ]]; then - echo "⚠ WARNING: No se encontró DNS Zone para $ZONE_VAR" - MISSING=1 - fi -done - -if [[ $MISSING -eq 1 ]]; then - echo "" - echo "Algunas DNS Zones no se encontraron. Verifica con:" - echo " az network private-dns zone list --resource-group $RG_NAME -o table" - echo "" - echo "El módulo puede usar nombres diferentes. Ajusta manualmente si es necesario." -fi - -echo "" -echo "Configurando azd environment en: $CONTENTFLOW_DIR" - -pushd "$CONTENTFLOW_DIR" > /dev/null - -azd env set DEPLOYMENT_MODE ailz-integrated -azd env set EXISTING_VNET_RESOURCE_ID "$VNET_ID" -azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$LAW_ID" -[[ -n "$BLOB_DNS" ]] && azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID "$BLOB_DNS" -[[ -n "$COSMOS_DNS" ]] && azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID "$COSMOS_DNS" -[[ -n "$APPCONFIG_DNS" ]] && azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID "$APPCONFIG_DNS" -[[ -n "$ACR_DNS" ]] && azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID "$ACR_DNS" -[[ -n "$COGNITIVE_DNS" ]] && azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID "$COGNITIVE_DNS" -[[ -n "$CONTAINERAPP_DNS" ]] && azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$CONTAINERAPP_DNS" - -popd > /dev/null - -echo "" -echo "✅ Configuración completada. Ejecuta:" -echo " cd $CONTENTFLOW_DIR && azd up" -``` - -### Mapeo Completo de Outputs → Variables azd - -| Output Terraform / Comando az | Variable azd ContentFlow | Descripción | -|---|---|---| -| `module.ailz.virtual_network.id` | `EXISTING_VNET_RESOURCE_ID` | Resource ID de la VNet | -| `module.ailz.log_analytics_workspace_id` | `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | LAW compartido | -| `az ... privatelink.blob.core.windows.net` | `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | DNS Zone para Blob | -| `az ... privatelink.documents.azure.com` | `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | DNS Zone para Cosmos | -| `az ... privatelink.azconfig.io` | `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | DNS Zone para App Config | -| `az ... privatelink.azurecr.io` | `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | DNS Zone para ACR | -| `az ... privatelink.cognitiveservices.azure.com` | `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | DNS Zone para AI Services | -| `az ... privatelink.azurecontainerapps.io` | `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | DNS Zone para Container Apps | -| (contenido en `pe-subnet`) | `PRIVATE_ENDPOINT_SUBNET_NAME` | Default: `pe-subnet` | -| (contenido en `aca-env-subnet`) | `CONTAINER_APPS_SUBNET_NAME` | Default: `aca-env-subnet` | - ---- - -## 7. Fix Conocido: storage_use_azuread - -### El Problema - -El módulo crea Storage Accounts con `shared_access_key_enabled = false` (default seguro). Sin embargo, el provider `azurerm` por defecto usa **autenticación por key** para operaciones de Storage. Esto produce: - -``` -Error: StatusCode=403 -- Original Error: autorest/azure: Service returned an error. -Status=403 Code="AuthorizationFailure" -``` - -### La Solución - -Agregar `storage_use_azuread = true` en el bloque del provider: - -```hcl -provider "azurerm" { - features { ... } - storage_use_azuread = true # ← CRÍTICO -} -``` - -### Prerrequisito - -El usuario que ejecuta `terraform apply` debe tener el rol **Storage Blob Data Contributor** en la suscripción o en los Resource Groups correspondientes: - -```bash -# Asignar rol a tu usuario (una vez) -az role assignment create \ - --assignee "$(az ad signed-in-user show --query id -o tsv)" \ - --role "Storage Blob Data Contributor" \ - --scope "/subscriptions/$(az account show --query id -o tsv)" -``` - ---- - -## 8. Paso a Paso: Despliegue - -### 8.1 Preparar el Entorno - -```bash -# Crear directorio de trabajo -mkdir mini-ailz-contentflow && cd mini-ailz-contentflow - -# Crear los archivos Terraform (copiar de la sección 5) -# terraform.tf, variables.tf, main.tf, ailz.tf, outputs.tf -``` - -### 8.2 Inicializar Terraform - -```bash -terraform init -``` - -Esto descarga el módulo AVM y todos sus sub-módulos (~20 módulos). - -### 8.3 Validar y Planificar - -```bash -# Validar sintaxis -terraform validate - -# Ver plan de ejecución -terraform plan -out=tfplan -``` - -El plan debería mostrar aproximadamente **15-20 recursos** a crear (vs ~45+ con el módulo completo). - -### 8.4 Aplicar - -```bash -terraform apply tfplan -``` - -Tiempo estimado: **15-25 minutos** (Bastion y Container Apps Environment son los más lentos). - -### 8.5 Verificar Recursos Creados - -```bash -# Listar recursos en el RG -az resource list \ - --resource-group rg-mini-ailz-contentflow \ - --query "[].{name:name, type:type}" \ - -o table -``` - -Deberías ver: -- Virtual Network -- Subredes (varias, creadas por el módulo) -- NSGs -- Private DNS Zones (6+) -- Bastion Host + Public IP -- Container Apps Environment -- Log Analytics Workspace -- Key Vault -- Container Registry -- Storage Account -- App Configuration -- Cosmos DB Account - -### 8.6 Configurar ContentFlow - -```bash -# Opción A: Script automatizado -chmod +x configure-contentflow.sh -./configure-contentflow.sh /path/to/contentflow - -# Opción B: Manual -cd /path/to/contentflow -azd env set DEPLOYMENT_MODE ailz-integrated -azd env set EXISTING_VNET_RESOURCE_ID "$(terraform -chdir=/path/to/mini-ailz output -raw vnet_resource_id)" -# ... (ver sección 6 para el mapeo completo) -``` - -### 8.7 Desplegar ContentFlow - -```bash -cd /path/to/contentflow -azd up -``` - ---- - -## 9. Comparación de Costo: Full vs Mini - -### Despliegue Completo del Módulo (tu test anterior en standalone-test001) - -| Recurso | SKU/Tier | Costo Estimado/mes | -|---|---|---| -| AI Foundry Hub + BYOR | S0 | ~$0 (pago por uso) | -| GPT-4.1 Model | GlobalStandard x1 | ~$0-30 (por tokens) | -| AI Search | Standard, 2 replicas | **~$500** | -| App Gateway (WAF_v2) | 2 scale units | **~$350** | -| Azure Bastion | Standard, 3 AZs | **~$350** | -| Azure Firewall | Standard, 3 AZs | **~$250** | -| Build VM | Standard_B2s | **~$30** | -| APIM | Premium x3 | **~$2,100** | -| Container Apps Env | Consumption (ZR) | ~$0-20 | -| Cosmos DB (BYOR) | Serverless | ~$0-10 | -| Cosmos DB (GenAI) | Serverless | ~$0-10 | -| Key Vault x2 | Standard | ~$0-5 | -| Storage Account x2 | ZRS/GRS | ~$5-15 | -| Container Registry | Premium (ZR) | **~$60** | -| App Configuration | Standard | ~$36 | -| Log Analytics | Per-GB | ~$0-20 | -| Bing Grounding | G1 | ~$5 | -| KS AI Search | Standard | **~$250** | -| VNet + Subredes | - | ~$0 | -| Private DNS Zones | - | ~$1-5 | -| **TOTAL ESTIMADO** | | **~$3,500-4,000/mes** | - -### Mini-AILZ (este documento) - -| Recurso | SKU/Tier | Costo Estimado/mes | -|---|---|---| -| Azure Bastion | **Basic**, sin AZs | **~$140** | -| Container Apps Env | Consumption, sin ZR | ~$0-20 | -| Container Registry | Premium (requerido para PE) | **~$50** | -| Cosmos DB (GenAI) | Serverless | ~$0-10 | -| Key Vault | Standard | ~$0-5 | -| Storage Account | **LRS** | ~$2-5 | -| App Configuration | Standard | ~$36 | -| Log Analytics | Per-GB | ~$0-20 | -| VNet + Subredes | - | ~$0 | -| Private DNS Zones | - | ~$1-5 | -| NSGs | - | ~$0 | -| **TOTAL ESTIMADO** | | **~$230-290/mes** | - -### Ahorro - -| Métrica | Full | Mini | Ahorro | -|---|---|---|---| -| Costo mensual | ~$3,700 | ~$260 | **~$3,440/mes (93%)** | -| Recursos Azure | ~40+ | ~15-20 | **50% menos** | -| Tiempo deploy | ~45 min | ~20 min | **55% menos** | - -> **Nota**: Los costos de ContentFlow en sí (Container Apps, Cosmos DB, Storage, etc. que crea `azd up`) son adicionales y similares en ambos escenarios (~$100-200/mes dependiendo del uso). - ---- - -## 10. Diagrama de Arquitectura Resultante - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Resource Group: rg-mini-ailz-contentflow │ -│ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ VNet (10.0.0.0/16) │ │ -│ │ │ │ -│ │ ┌─────────────────┐ ┌──────────────────────────────────────┐ │ │ -│ │ │ Bastion Subnet │ │ Container Apps Subnet (/23) │ │ │ -│ │ │ ┌────────────┐ │ │ │ │ │ -│ │ │ │ Bastion │ │ │ ┌──────────────────────────────┐ │ │ │ -│ │ │ │ (Basic) │ │ │ │ Container Apps Environment │ │ │ │ -│ │ │ └────────────┘ │ │ │ (Internal Load Balancer) │ │ │ │ -│ │ └─────────────────┘ │ │ │ │ │ │ │ -│ │ │ │ │ ← ContentFlow despliega: │ │ │ │ -│ │ ┌─────────────────┐ │ │ │ API / Worker / Web │ │ │ │ -│ │ │ PE Subnet │ │ │ └──────────────────────────────┘ │ │ │ -│ │ │ │ │ └──────────────────────────────────────┘ │ │ -│ │ │ ← ContentFlow │ │ │ │ -│ │ │ crea PEs: │ │ ┌──────────────────────────────────────┐ │ │ -│ │ │ ● Blob Storage │ │ │ Plataforma AILZ (módulo) │ │ │ -│ │ │ ● Cosmos DB │ │ │ │ │ │ -│ │ │ ● App Config │ │ │ ● Log Analytics Workspace │ │ │ -│ │ │ ● ACR │ │ │ ● Key Vault (GenAI) │ │ │ -│ │ │ ● AI Foundry │ │ │ ● Container Registry (GenAI) │ │ │ -│ │ └─────────────────┘ │ │ ● Storage Account (GenAI, LRS) │ │ │ -│ │ │ │ ● App Configuration (GenAI) │ │ │ -│ └───────────────────────│ │ ● Cosmos DB (GenAI) │ │ │ -│ │ │ ● NSGs │ │ │ -│ ┌────────────────────┐ │ └──────────────────────────────────────┘ │ │ -│ │ Private DNS Zones │ │ │ │ -│ │ (auto-vinculadas) │ └────────────────────────────────────────────┘ │ -│ │ │ │ -│ │ ● privatelink.blob.core.windows.net │ -│ │ ● privatelink.documents.azure.com │ -│ │ ● privatelink.azconfig.io │ -│ │ ● privatelink.azurecr.io │ -│ │ ● privatelink.cognitiveservices.azure.com │ -│ │ ● privatelink.azurecontainerapps.io │ -│ │ ● (+ otras del módulo: vault, openai, etc.) │ -│ └────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────────────┐ -│ Resource Group: rg-contentflow-ailz (creado por azd up) │ -│ │ -│ ● Storage Account (Blob + Queue) + Private Endpoints en pe-subnet │ -│ ● Cosmos DB + Private Endpoint en pe-subnet │ -│ ● App Configuration + Private Endpoint en pe-subnet │ -│ ● ACR Premium + Private Endpoint en pe-subnet │ -│ ● AI Foundry (Cognitive Services) + Private Endpoint en pe-subnet │ -│ ● Container Apps (API, Worker, Web) → Container Apps Environment │ -│ ● Application Insights → Log Analytics Workspace (compartido) │ -│ ● User Assigned Managed Identity + Role Assignments │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 11. Notas de Operación y Troubleshooting - -### 11.1 Nombres de Subredes - -El módulo AVM genera nombres de subredes internamente. ContentFlow espera `pe-subnet` y `aca-env-subnet`. Si los nombres generados por el módulo no coinciden, puedes: - -1. **Verificar nombres reales** después del apply: - ```bash - terraform output subnets - ``` - -2. **Configurar en ContentFlow** los nombres reales: - ```bash - azd env set PRIVATE_ENDPOINT_SUBNET_NAME "" - azd env set CONTAINER_APPS_SUBNET_NAME "" - ``` - -3. **Override en vnet_definition** (si el módulo lo permite): - ```hcl - vnet_definition = { - address_space = ["10.0.0.0/16"] - subnets = { - pe-subnet = { - name = "pe-subnet" - address_prefix = "10.0.1.0/27" - } - aca-env-subnet = { - name = "aca-env-subnet" - address_prefix = "10.0.16.0/23" - } - } - } - ``` - -### 11.2 Container Apps Environment Dual - -Si el módulo crea un Container Apps Environment pero ContentFlow también intenta crear uno durante `azd up`, podrías terminar con dos CAEs. Para evitar esto: - -- **Opción A**: Configurar ContentFlow para usar el CAE existente (pasar su ID como variable de entorno) -- **Opción B**: Deshabilitar el CAE en el módulo (`deploy = false` en `container_app_environment_definition`) y dejar que ContentFlow lo cree - -### 11.3 Error: "Address space conflict" - -Si ves errores sobre conflictos de rango de IP: - -``` -Error: creating Virtual Network: the address space "192.168.0.0/20" overlaps with... -``` - -Asegúrate de usar un rango que **no esté en uso** en tu suscripción. Recomendamos `10.0.0.0/16` en lugar de `192.168.0.0/x`. - -### 11.4 Error: "Insufficient subnet size" - -Container Apps Environment requiere una subred de al menos `/23` (512 IPs). El módulo normalmente calcula esto correctamente, pero si haces override de subredes, asegúrate de respetar este mínimo. - -### 11.5 Terraform Destroy: Orden de Dependencias - -Para destruir la Mini-AILZ: - -```bash -# PRIMERO: Destruir ContentFlow (tiene dependencias en la red) -cd /path/to/contentflow -azd down --force --purge - -# SEGUNDO: Destruir la Mini-AILZ -cd /path/to/mini-ailz -terraform destroy -``` - -> **Importante**: Si intentas destruir la Mini-AILZ mientras ContentFlow tiene Private Endpoints activos en las subredes, el destroy fallará. Siempre destruye ContentFlow primero. - -### 11.6 Restricción de Address Space para Foundry CapabilityHost - -Si alguna vez necesitas habilitar AI Foundry con agent service (`capabilityHost`), hay una restricción conocida: - -> El address space de la VNet **no puede ser** `192.168.0.0/16` (pero sí puede ser ranges dentro de él como `192.168.0.0/20`). Otros rangos RFC1918 como `10.0.0.0/8` y `172.16.0.0/12` **sí funcionan correctamente**. - -Nuestro rango `10.0.0.0/16` no tiene este problema. - ---- - -## 12. Limpieza de Recursos - -### Limpieza Completa (ContentFlow + Mini-AILZ) - -```bash -# 1. Destruir ContentFlow primero -cd /path/to/contentflow -azd down --force --purge - -# 2. Destruir Mini-AILZ -cd /path/to/mini-ailz -terraform destroy -auto-approve - -# 3. Verificar que el RG fue eliminado -az group show --name rg-mini-ailz-contentflow 2>/dev/null && echo "RG aún existe" || echo "RG eliminado" -``` - -### Limpieza Solo ContentFlow (mantener Mini-AILZ) - -```bash -# Solo destruir ContentFlow, la red permanece para reusar -cd /path/to/contentflow -azd down --force --purge -``` - -### Purge de Recursos con Soft-Delete - -Algunos recursos Azure tienen soft-delete. Si necesitas re-crear con el mismo nombre: - -```bash -# Key Vault -az keyvault purge --name - -# Cognitive Services -az cognitiveservices account purge \ - --location \ - --resource-group \ - --name - -# App Configuration -az appconfig purge --name -``` - ---- - -## Referencia Rápida - -### Versiones Utilizadas - -| Componente | Versión | -|---|---| -| Terraform | >= 1.9, < 2.0 | -| AVM Pattern Module | 0.4.0 | -| azurerm provider | >= 3.116, < 5.0 | -| azapi provider | ~> 2.0 | -| ContentFlow | Modo `ailz-integrated` | - -### Documentos Relacionados - -- [01-overview.md](01-overview.md) — Visión general de ContentFlow -- [02-architecture-detailed.md](02-architecture-detailed.md) — Arquitectura técnica detallada -- [03-use_cases_examples.md](03-use_cases_examples.md) — Casos de uso y ejemplos -- [04-ai_lz_options.md](04-ai_lz_options.md) — Guía de despliegue AILZ (Azure CLI manual) -- **[Módulo AVM en GitHub](https://github.com/Azure/terraform-azurerm-avm-ptn-aiml-landing-zone)** — Código fuente y documentación completa -- **[Módulo AVM en Terraform Registry](https://registry.terraform.io/modules/Azure/avm-ptn-aiml-landing-zone/azurerm/latest)** — Registry oficial - ---- - -> **Última actualización**: Julio 2025 -> **Módulo fuente**: `Azure/avm-ptn-aiml-landing-zone/azurerm` v0.4.0 -> **Autor**: Análisis técnico para demos ContentFlow con suscripción única diff --git a/personal-private-configurations.md b/personal-private-configurations.md deleted file mode 100644 index 4ec4a38..0000000 --- a/personal-private-configurations.md +++ /dev/null @@ -1,141 +0,0 @@ -# Personal Private Configurations - -> **WARNING**: This file contains environment-specific configurations. Do NOT commit to a shared/public repository. - ---- - -## Variables - -Update these values to reuse all scripts below in any environment. - -```bash -# ── Environment ── -RESOURCE_GROUP="rg-contentflow-test-001" -SUBSCRIPTION_ID="d12d19a9-0636-4951-90a4-339158fd57d8" - -# ── Container App names ── -APP_API="api-vtkhgrhgdl4w2" -APP_WEB="web-vtkhgrhgdl4w2" -APP_WORKER="worker-vtkhgrhgdl4w2" -APPS=("$APP_API" "$APP_WEB" "$APP_WORKER") - -# ── Networking ── -MY_IP=$(curl -s ifconfig.me) -RULE_NAME="allow-my-ip" - -# ── Endpoints ── -API_URL="https://api-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" -WEB_URL="https://web-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" -WORKER_URL="https://worker-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" -``` - ---- - -## IP Access Restrictions on Azure Container Apps - -**Date Applied**: March 16, 2026 -**Resource Group**: `rg-contentflow-test-001` -**Subscription**: `ME-MngEnvMCAP887462-gereyeso-1` (`d12d19a9-0636-4951-90a4-339158fd57d8`) -**Region**: East US 2 - -### What was configured - -All three Container Apps were restricted to accept external ingress traffic **only** from a single IP address. Any request from a different IP receives a `403 Forbidden` response. - -| Container App | Allowed IP | Rule Name | -|---------------|-----------|-----------| -| `api-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | -| `web-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | -| `worker-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | - -### Service Endpoints (restricted) - -| Service | URL | -|---------|-----| -| Web UI | `$WEB_URL` | -| API | `$API_URL` | -| API Docs | `$API_URL/docs` | -| Worker | `$WORKER_URL` | - -### Why this is safe for internal communication - -Container Apps within the **same Container Apps Environment** communicate through the internal network. IP access restrictions apply only to **external** ingress traffic (from the internet). The service-to-service flow remains unaffected: - -``` -Browser (your IP) → Web UI → (browser fetches from) → API → (internal) → Worker -``` - -- **Web → API**: The React SPA runs in your browser, so API calls originate from your IP. -- **API → Worker**: Both are in the same Container Apps Environment; internal traffic bypasses IP restrictions. - ---- - -### Apply IP restriction to all apps - -```bash -for app in "${APPS[@]}"; do - echo "Restricting $app to $MY_IP ..." - az containerapp ingress access-restriction set \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --rule-name "$RULE_NAME" \ - --ip-address "$MY_IP/32" \ - --action Allow \ - --description "Allow only my IP" -done -``` - -### Update IP on all apps (when your IP changes) - -```bash -MY_IP=$(curl -s ifconfig.me) -echo "New IP: $MY_IP" - -for app in "${APPS[@]}"; do - echo "Updating $app ..." - az containerapp ingress access-restriction remove \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --rule-name "$RULE_NAME" - - az containerapp ingress access-restriction set \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --rule-name "$RULE_NAME" \ - --ip-address "$MY_IP/32" \ - --action Allow \ - --description "Allow only my IP" -done -``` - -### Remove all restrictions (make public again) - -```bash -for app in "${APPS[@]}"; do - echo "Removing restriction from $app ..." - az containerapp ingress access-restriction remove \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --rule-name "$RULE_NAME" -done -``` - -### Verify current restrictions - -```bash -for app in "${APPS[@]}"; do - echo "=== $app ===" - az containerapp ingress access-restriction list \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --output table -done -``` - -### Quick health check - -```bash -for url in "$API_URL/health" "$WEB_URL" "$WORKER_URL"; do - echo "$url → $(curl -s -o /dev/null -w '%{http_code}' "$url")" -done -``` From fc391c4fe5db3847b83f566be78b79530a4834cd Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Fri, 27 Mar 2026 14:41:33 -0400 Subject: [PATCH 23/29] changes from azd provision --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 8 +++++++- 2 files changed, 7 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 18783b35c1162015272d62740bd922d6f9247c96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMOKTHR6h1eJHQ=Jvg%lS$Kfni7yHlE$2)1s7q6mT{X<{49izGE^K^Sl)bfbtn zm9BKHvlDj7=EpYpbBR zr>-7^feKS%2n|QSC*5Ha#`@Z7I0+3WVIm7tp$HWnI8V8gNNDTMP64MtPyt!HkI`XD z(>kpN>-T+cy8DaeWHM7IXK+3K@y`Bc`tj@8pig$7Pi2Mg6R7Qi7n0X$2Z&cyyw-n?VoG;rfI1 z>6DJqb)RCn!zA2`^C9});&YX1v_xgf26j~D4Ik#g`S|B<9%FnguMVGZFPo2go5pFD zN|+%9S_7**)hvagCw_rZz=v~s6u z&-q+z-fn4R-Gh%!MGiXE=`xfmQ-cnHUxo6PT9NtRU^W23xjwyi zzr}SRs-xuaD4q{RQy|xeT>+zSKOfVVJ`6_id=7kRww{;pd{i7h8pETkd`_x)5+0|4 z5wr8zy*b`DABEug9Dg-af2p7U0daUf!=tQx_NG9N_Jq#22IgIrt{|(bHS5IJ(y%z5 z+viUYTXS!ym^{N#u~wcDU*@?3v#o@usY+=(n~iUFFXudQCpRhU|5M@n|65rCmzz^y+bbYq$;IRxWH7aLKoD7LdpJ@!WRrfrwhDriD#u|} zIS$+YharA@aFv*ZvA(vrg7(ih0{HitTmSWUD*JBWYw4(KIA3K)#oFtyFQ*1uzuo#T I1^Jup4?b@#^#A|> diff --git a/.gitignore b/.gitignore index 1f51dd7..d3afc3e 100644 --- a/.gitignore +++ b/.gitignore @@ -208,5 +208,11 @@ __marimo__/ gitignore* +# macOS +.DS_Store +.DS_Store? +._* + # Local analysis files -.Analysis/ +Analysis/ +.Analysis/ \ No newline at end of file From 6ac0335a0e39afcbb272fa976e33f6006372a8c7 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Sun, 29 Mar 2026 08:46:21 -0400 Subject: [PATCH 24/29] content_flow_appgateway --- infra/bicep/main.bicep | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index a524235..6cf9440 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -709,3 +709,12 @@ output AI_SERVICES_NAME string = aiFoundry.outputs.aiServicesName output VNET_RESOURCE_ID string = isAILZIntegrated ? existingVnetResourceId : '' output PRIVATE_ENDPOINT_SUBNET_ID string = isAILZIntegrated ? networkConfig.privateEndpointSubnetId : '' output CONTAINER_APPS_SUBNET_ID string = isAILZIntegrated ? networkConfig.containerAppsSubnetId : '' + +// Container Apps Environment outputs +// Required by platform team to configure Application Gateway routing to internal CAE: +// - CAE_STATIC_IP: Private IP assigned to the CAE infrastructure, used as Application Gateway backend pool target +// - CAE_DEFAULT_DOMAIN: Dynamic domain suffix for the CAE, used to create the Private DNS Zone and backend HTTPS host headers +output CAE_STATIC_IP string = containerAppsEnvironment.outputs.staticIp +output CAE_DEFAULT_DOMAIN string = containerAppsEnvironment.outputs.defaultDomain +// - RESOURCE_TOKEN: Unique suffix used in all resource names, needed to construct Container App FQDNs for App Gateway backend host headers +output RESOURCE_TOKEN string = resourceToken From 4e3d993d31975adc58e77e56a38c7ef84a157ab7 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Sun, 29 Mar 2026 12:31:42 -0400 Subject: [PATCH 25/29] content_flow ailz integrated fixes --- Changes.md | 332 +++++++++++++++++++++++++++++++ azure.yaml | 7 + infra/bicep/main.bicep | 10 +- infra/bicep/main.parameters.json | 3 + infra/scripts/pre-deploy.sh | 113 +++++++++++ 5 files changed, 461 insertions(+), 4 deletions(-) diff --git a/Changes.md b/Changes.md index 8b252b7..5e7a237 100644 --- a/Changes.md +++ b/Changes.md @@ -3,6 +3,9 @@ ## Table of Contents - [1. Fix: `remoteBuild: true` Incompatible with Private ACR](#1-fix-remotebuild-true-incompatible-with-private-acr) +- [2. Fix: ACR Pull Fails on Fresh Environment Due to RBAC Propagation Delay](#2-fix-acr-pull-fails-on-fresh-environment-due-to-rbac-propagation-delay) +- [3. Fix: Container Apps `ingress.external: false` Blocks VNet Traffic in Internal CAE](#3-fix-container-apps-ingressexternal-false-blocks-vnet-traffic-in-internal-cae) +- [4. Fix: Queue Private Endpoint Missing DNS A Record](#4-fix-queue-private-endpoint-missing-dns-a-record) --- @@ -142,3 +145,332 @@ The previous check only verified that the Docker CLI binary was installed, not t > Builds Docker images locally (requires Docker daemon running on the machine executing `azd`) > Pushes to ACR (via public endpoint in `basic` mode, via private endpoint in `ailz-integrated` mode — must be executed from within the AI LZ VNet, e.g. from the JumpBox VM) > Updates Container Apps + +--- + +## 2. Fix: ACR Pull Fails on Fresh Environment Due to RBAC Propagation Delay + +**Error:** + +``` +Failed to provision revision for container app 'api-xmi4h2hmzcfbk'. +Error details: Invalid value: "crxmi4h2hmzcfbk.azurecr.io/contentflow/api-content_flow_sunday29:azd-deploy-1774789666": +unable to pull image using Managed identity +/subscriptions/.../providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-xmi4h2hmzcfbk +for registry crxmi4h2hmzcfbk.azurecr.io +``` + +**Root Cause:** + +When `azd up` runs on a fresh environment, Bicep creates the Managed Identity, the ACR, and the `AcrPull` role assignment in a single ARM deployment. ARM reports success as soon as the role assignment _resource_ exists. However, Azure RBAC propagation is **eventually consistent** — the `AcrPull` permission can take up to 10 minutes to be effective across all Azure data planes. + +`azd up` proceeds immediately from `provision` to `deploy`. During deploy, `azd` pushes the image to ACR (succeeds because push uses the CLI's own credentials), then updates the Container App revision. The Container App infrastructure tries to pull the image using the Managed Identity's `AcrPull` role, but the role has not yet propagated — resulting in the pull failure above. + +This affects **both deployment modes** (`basic` and `ailz-integrated`) because both use Managed Identity + `AcrPull` for registry access. The failure is more frequent in `ailz-integrated` mode because the private endpoint adds additional resolution latency. + +**Files:** `azure.yaml`, `infra/scripts/pre-deploy.sh` + +--- + +### `azure.yaml` — Add global `predeploy` hook + +**Before:** + +```yaml +# Global hooks +hooks: + preprovision: + shell: sh + run: ./infra/scripts/pre-provision.sh + interactive: false + continueOnError: false + + postprovision: + shell: sh + run: ./infra/scripts/post-provision.sh + interactive: false + continueOnError: false + + postdeploy: + shell: sh + run: ./infra/scripts/post-deploy.sh + interactive: true + continueOnError: false +``` + +**After:** + +```yaml +# Global hooks +hooks: + preprovision: + shell: sh + run: ./infra/scripts/pre-provision.sh + interactive: false + continueOnError: false + + postprovision: + shell: sh + run: ./infra/scripts/post-provision.sh + interactive: false + continueOnError: false + + # Run before deploying services - validates ACR pull readiness + predeploy: + shell: sh + run: ./infra/scripts/pre-deploy.sh + interactive: false + continueOnError: false + + postdeploy: + shell: sh + run: ./infra/scripts/post-deploy.sh + interactive: true + continueOnError: false +``` + +The `predeploy` hook already existed as a script (`infra/scripts/pre-deploy.sh`) but was **not wired** into `azure.yaml`. Now it runs automatically before `azd` deploys any service — both in `azd up` and standalone `azd deploy`. + +--- + +### `infra/scripts/pre-deploy.sh` — ACR Pull Readiness Gate + +**Before:** + +```bash +#!/bin/bash +# Pre-deploy hook - runs before deploying all services +set -e + +# ... basic infrastructure check only ... + +if ! azd env get-value AZURE_RESOURCE_GROUP &> /dev/null; then + echo "❌ Infrastructure not provisioned. Please run 'azd provision' first." + exit 1 +fi + +echo "✓ Infrastructure is ready for deployment" +``` + +**After:** + +The script now performs a 3-phase ACR readiness validation: + +**Phase 1 — AcrPull role visibility polling (both modes)** + +Reads `AZURE_CONTAINER_REGISTRY_NAME` and `MANAGED_IDENTITY_PRINCIPAL_ID` from `azd env` outputs. Polls `az role assignment list` every 10 seconds until `AcrPull` appears for the principal on the ACR scope. Timeout defaults to 300 seconds. If the role is already visible (e.g. second deploy on existing environment), passes immediately with no delay. + +```bash +ROLES=$(az role assignment list \ + --assignee-object-id "$IDENTITY_PRINCIPAL_ID" \ + --scope "$ACR_RESOURCE_ID" \ + --query "[?roleDefinitionName=='AcrPull'].roleDefinitionName" \ + -o tsv 2>/dev/null || echo "") +``` + +**Phase 2 — ACR Private Endpoint DNS verification (AILZ only)** + +In `ailz-integrated` mode, verifies that the ACR login server resolves to a private IP (RFC 1918 range). If it resolves to a public IP or doesn't resolve, emits a warning. This catches misconfigured `privatelink.azurecr.io` DNS zones early. + +**Phase 3 — RBAC stabilization wait (both modes)** + +After `AcrPull` is visible in the control plane, waits a configurable stabilization period (default: 60 seconds) before allowing deploy. This compensates for the gap between role assignment queryability and data plane effectiveness. + +**Configurability:** + +| Variable | Default | Override | +|---|---|---| +| `ACR_RBAC_TIMEOUT` | 300s | `ACR_RBAC_TIMEOUT=600 azd deploy` | +| `ACR_RBAC_STABILIZATION` | 60s | `ACR_RBAC_STABILIZATION=0 azd deploy` | + +On an existing environment where RBAC is already propagated, the script detects `AcrPull` immediately and only waits the stabilization period. For subsequent deploys where no RBAC change occurred, the stabilization can be set to 0. + +**Failure behavior:** + +If the timeout is reached without finding `AcrPull`, the script exits with code 1 and prints: +- The ACR name and principal ID that failed +- The elapsed wait time +- The retry command with instructions to increase timeout + +--- + +## 3. Fix: Container Apps `ingress.external: false` Blocks VNet Traffic in Internal CAE + +**Error:** + +From JumpBox or Application Gateway attempting to reach any Container App: + +``` +HTTP 404 — site not found +``` + +The Container Apps are running and healthy, but unreachable from VNet resources outside the CAE. + +**Root Cause:** + +All three Container Apps (API, Worker, Web) were provisioned with `externalIngress: !isAILZIntegrated`, which evaluates to `false` in `ailz-integrated` mode. In an **internal** Container Apps Environment (`internal: true`), the `ingress.external` flag controls VNet-level visibility: + +| `ingress.external` | Internal CAE behavior | +|---|---| +| `true` | Accessible from **any resource in the VNet** (App Gateway, JumpBox, other subnets) | +| `false` | Accessible **only from other Container Apps in the same CAE** | + +With `external: false`, the App Gateway, JumpBox, and any other VNet-hosted client receives HTTP 404 because the CAE reverse proxy refuses to route traffic from non-CAE sources. + +Critically, in an internal CAE **neither setting exposes the app to the internet** — the CAE itself has no public IP. Setting `external: true` in an internal CAE simply opens VNet-level routing, which is exactly what's needed for Application Gateway integration. + +**Impact on both deployment modes:** + +| Mode | Before | After | Net change | +|---|---|---|---| +| `basic` | `externalIngress: true` (public CAE, internet-facing) | `externalIngress: true` | **None** — already `true` | +| `ailz-integrated` | `externalIngress: false` (internal CAE, CAE-only) | `externalIngress: true` (internal CAE, VNet-accessible) | **VNet resources can now reach the apps** — no internet exposure | + +**Files:** `infra/bicep/main.bicep` + +--- + +### `main.bicep` — Change `externalIngress` to `true` for all three Container Apps + +**Before (API, line ~578):** + +```bicep +externalIngress: !isAILZIntegrated +``` + +**After:** + +```bicep +externalIngress: true +``` + +The same change was applied to: +- **API** Container App (`apiContainerApp`) +- **Worker** Container App (`workerContainerApp`) +- **Web** Container App (`webContainerApp`) + +All three previously had `externalIngress: !isAILZIntegrated`. Now all three have `externalIngress: true`. The `container-app.bicep` module default was already `true` — the override in `main.bicep` was forcing it to `false` in AILZ mode. + +--- + +## 4. Fix: Queue Private Endpoint Missing DNS A Record + +**Error:** + +Worker container crashes on startup validation: + +``` +azure.core.exceptions.HttpResponseError: Unable to resolve host queue endpoint +``` + +Or: the queue private endpoint exists but `nslookup .queue.core.windows.net` returns a public IP instead of the private endpoint IP, causing timeouts in a private-network-only environment. + +**Root Cause:** + +Three compounding gaps prevented the queue private DNS zone ID from reaching the storage module: + +1. **`main.parameters.json`** — Missing `existingQueuePrivateDnsZoneId` mapping. The `get-ailz-resources.sh` script discovers the value and sets `EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID` in the azd environment, but the parameters file never passed it to Bicep. + +2. **`main.bicep` storage module call** — The `queuePrivateDnsZoneId` parameter was never passed. The `blobPrivateDnsZoneId` was correctly passed, but the queue equivalent was missing. + +3. **`main.bicep` storageQueueDnsZoneGroup condition** — The condition `isAILZIntegrated && !empty(existingQueuePrivateDnsZoneId)` was too restrictive. Since the parameter was always empty (due to gaps 1 and 2), this fallback DNS zone group module never executed. + +Additionally, the AILZ validation block did not validate the presence of `existingQueuePrivateDnsZoneId`, so the deployment proceeded silently without the queue DNS zone. + +**Impact on both deployment modes:** + +| Mode | Before | After | Net change | +|---|---|---|---| +| `basic` | No private endpoints, public access | No private endpoints, public access | **None** — queue PE not used in basic mode | +| `ailz-integrated` | Queue PE created but no DNS A record → worker crash | Queue PE created **with** DNS A record → worker resolves correctly | **Queue endpoint resolves to private IP** | + +**Files:** `infra/bicep/main.bicep`, `infra/bicep/main.parameters.json` + +--- + +### `main.parameters.json` — Add queue private DNS zone ID mapping + +**Before:** + +```json +"existingBlobPrivateDnsZoneId": { + "value": "${EXISTING_BLOB_PRIVATE_DNS_ZONE_ID=}" +}, +"existingCosmosPrivateDnsZoneId": { +``` + +**After:** + +```json +"existingBlobPrivateDnsZoneId": { + "value": "${EXISTING_BLOB_PRIVATE_DNS_ZONE_ID=}" +}, +"existingQueuePrivateDnsZoneId": { + "value": "${EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID=}" +}, +"existingCosmosPrivateDnsZoneId": { +``` + +The `=` suffix (empty default) ensures basic mode deployments pass an empty string, which is the expected no-op value. + +--- + +### `main.bicep` — Pass `queuePrivateDnsZoneId` to storage module + +**Before:** + +```bicep +blobPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.blob : '' +publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' +``` + +**After:** + +```bicep +blobPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.blob : '' +queuePrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.queue : '' +publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' +``` + +Uses the same pattern as `blobPrivateDnsZoneId`: pass the DNS zone ID from `networkConfig` in AILZ mode, empty string in basic mode. The `networkConfig` variable already had the `queue` key mapped to `existingQueuePrivateDnsZoneId`. + +--- + +### `main.bicep` — Simplify `storageQueueDnsZoneGroup` condition + +**Before:** + +```bicep +module storageQueueDnsZoneGroup ... = if (isAILZIntegrated && !empty(existingQueuePrivateDnsZoneId)) { +``` + +**After:** + +```bicep +module storageQueueDnsZoneGroup ... = if (isAILZIntegrated) { +``` + +The `!empty()` guard was redundant because the AILZ validation block now fails the deployment if the queue DNS zone ID is missing. Simplifying the condition makes it consistent with the `storageBlobDnsZoneGroup` module, which already used `if (isAILZIntegrated)`. + +--- + +### `main.bicep` — Add queue DNS zone validation to AILZ block + +**Before:** + +```bicep +acrPrivateDnsZoneRequired: !empty(existingAcrPrivateDnsZoneId) ?? fail(...) +containerAppsEnvPrivateDnsZoneRequired: !empty(existingContainerAppsEnvPrivateDnsZoneId) ?? fail(...) +} : {} +``` + +**After:** + +```bicep +acrPrivateDnsZoneRequired: !empty(existingAcrPrivateDnsZoneId) ?? fail(...) +containerAppsEnvPrivateDnsZoneRequired: !empty(existingContainerAppsEnvPrivateDnsZoneId) ?? fail(...) +queuePrivateDnsZoneRequired: !empty(existingQueuePrivateDnsZoneId) ?? fail('existingQueuePrivateDnsZoneId is required for ailz-integrated mode') +} : {} +``` + +This ensures AILZ deployments fail fast with a clear error message if the queue private DNS zone ID is not provided, instead of silently deploying with a broken queue endpoint. diff --git a/azure.yaml b/azure.yaml index 7d76425..a0b3125 100644 --- a/azure.yaml +++ b/azure.yaml @@ -64,6 +64,13 @@ hooks: interactive: false continueOnError: false + # Run before deploying services - validates ACR pull readiness + predeploy: + shell: sh + run: ./infra/scripts/pre-deploy.sh + interactive: false + continueOnError: false + # Run after all services are deployed postdeploy: shell: sh diff --git a/infra/bicep/main.bicep b/infra/bicep/main.bicep index 6cf9440..cc0f5b7 100644 --- a/infra/bicep/main.bicep +++ b/infra/bicep/main.bicep @@ -112,6 +112,7 @@ var ailzValidation = isAILZIntegrated ? { appConfigPrivateDnsZoneRequired: !empty(existingAppConfigPrivateDnsZoneId) ?? fail('existingAppConfigPrivateDnsZoneId is required for ailz-integrated mode') acrPrivateDnsZoneRequired: !empty(existingAcrPrivateDnsZoneId) ?? fail('existingAcrPrivateDnsZoneId is required for ailz-integrated mode') containerAppsEnvPrivateDnsZoneRequired: !empty(existingContainerAppsEnvPrivateDnsZoneId) ?? fail('existingContainerAppsEnvPrivateDnsZoneId is required for ailz-integrated mode') + queuePrivateDnsZoneRequired: !empty(existingQueuePrivateDnsZoneId) ?? fail('existingQueuePrivateDnsZoneId is required for ailz-integrated mode') } : {} // ========== VARIABLES ========== @@ -222,6 +223,7 @@ module storage 'modules/storage.bicep' = { enablePrivateEndpoint: isAILZIntegrated privateEndpointSubnetId: isAILZIntegrated ? networkConfig.privateEndpointSubnetId : '' blobPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.blob : '' + queuePrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.queue : '' publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' logAnalyticsWorkspaceId: logAnalyticsWorkspaceId tags: tags @@ -244,7 +246,7 @@ module storageBlobDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = ] } -module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated && !empty(existingQueuePrivateDnsZoneId)) { +module storageQueueDnsZoneGroup 'modules/private-endpoint-dns-zone-group.bicep' = if (isAILZIntegrated) { name: 'storage-queue-dns-zone-group-${resourceToken}' params: { privateEndpointName: '${storageAccountName}-queue-pe' @@ -575,7 +577,7 @@ module apiContainerApp 'modules/container-app.bicep' = { containerRegistryServer: containerRegistry.outputs.loginServer managedIdentityId: userAssignedIdentity.outputs.resourceId targetPort: 8090 - externalIngress: !isAILZIntegrated + externalIngress: true corsEnabled: true livenessProbePath: '/' cpuCores: 2 @@ -613,7 +615,7 @@ module workerContainerApp 'modules/container-app.bicep' = { containerRegistryServer: containerRegistry.outputs.loginServer managedIdentityId: userAssignedIdentity.outputs.resourceId targetPort: workerContainerAppTargetPort - externalIngress: !isAILZIntegrated + externalIngress: true corsEnabled: true livenessProbePath: '/' cpuCores: 2 @@ -651,7 +653,7 @@ module webContainerApp 'modules/container-app.bicep' = { containerRegistryServer: containerRegistry.outputs.loginServer managedIdentityId: userAssignedIdentity.outputs.resourceId targetPort: 8080 - externalIngress: !isAILZIntegrated + externalIngress: true corsEnabled: true livenessProbePath: '/' cpuCores: 1 diff --git a/infra/bicep/main.parameters.json b/infra/bicep/main.parameters.json index 57ef45e..9f8479a 100644 --- a/infra/bicep/main.parameters.json +++ b/infra/bicep/main.parameters.json @@ -32,6 +32,9 @@ "existingBlobPrivateDnsZoneId": { "value": "${EXISTING_BLOB_PRIVATE_DNS_ZONE_ID=}" }, + "existingQueuePrivateDnsZoneId": { + "value": "${EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID=}" + }, "existingCosmosPrivateDnsZoneId": { "value": "${EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID=}" }, diff --git a/infra/scripts/pre-deploy.sh b/infra/scripts/pre-deploy.sh index 9cb0d50..47a8176 100755 --- a/infra/scripts/pre-deploy.sh +++ b/infra/scripts/pre-deploy.sh @@ -1,5 +1,6 @@ #!/bin/bash # Pre-deploy hook - runs before deploying all services +# Validates ACR pull readiness to prevent RBAC propagation failures set -e echo "==================================================" @@ -16,6 +17,118 @@ fi echo "✓ Infrastructure is ready for deployment" +# ────────────────────────────────────────────────────────── +# ACR Pull Readiness Gate +# ────────────────────────────────────────────────────────── +# After azd provision, the AcrPull role assignment exists in ARM but Azure RBAC +# propagation is eventually consistent. Container Apps may fail to pull images +# if the role is not yet effective. This gate blocks deploy until readiness is +# confirmed, preventing the intermittent "unable to pull image using Managed +# identity" error on fresh environments. +# ────────────────────────────────────────────────────────── + +ACR_NAME=$(azd env get-value AZURE_CONTAINER_REGISTRY_NAME 2>/dev/null || echo "") +IDENTITY_PRINCIPAL_ID=$(azd env get-value MANAGED_IDENTITY_PRINCIPAL_ID 2>/dev/null || echo "") +DEPLOYMENT_MODE=$(azd env get-value DEPLOYMENT_MODE 2>/dev/null || echo "basic") + +# Configurable via azd env set (seconds) +ACR_RBAC_TIMEOUT=${ACR_RBAC_TIMEOUT:-300} +ACR_RBAC_STABILIZATION=${ACR_RBAC_STABILIZATION:-60} + +if [ -z "$ACR_NAME" ] || [ -z "$IDENTITY_PRINCIPAL_ID" ]; then + echo "⚠ ACR_NAME or MANAGED_IDENTITY_PRINCIPAL_ID not found in azd env." + echo " Skipping ACR readiness check. Deploy may fail if RBAC is not yet propagated." +else + echo "" + echo "── ACR Pull Readiness Check ──" + echo " Registry: $ACR_NAME" + echo " Principal ID: $IDENTITY_PRINCIPAL_ID" + echo " Mode: $DEPLOYMENT_MODE" + echo " Timeout: ${ACR_RBAC_TIMEOUT}s" + echo " Stabilization: ${ACR_RBAC_STABILIZATION}s" + echo "" + + # Get ACR resource ID for scoped role query + ACR_RESOURCE_ID=$(az acr show -n "$ACR_NAME" --query id -o tsv 2>/dev/null || echo "") + + if [ -z "$ACR_RESOURCE_ID" ]; then + echo "⚠ Could not resolve ACR resource ID for '$ACR_NAME'. Skipping readiness check." + else + # ── Phase 1: Wait for AcrPull role to be visible ── + echo " Checking AcrPull role assignment..." + ELAPSED=0 + POLL_INTERVAL=10 + ACR_PULL_FOUND=false + + while [ "$ELAPSED" -lt "$ACR_RBAC_TIMEOUT" ]; do + ROLES=$(az role assignment list \ + --assignee-object-id "$IDENTITY_PRINCIPAL_ID" \ + --scope "$ACR_RESOURCE_ID" \ + --query "[?roleDefinitionName=='AcrPull'].roleDefinitionName" \ + -o tsv 2>/dev/null || echo "") + + if echo "$ROLES" | grep -q "AcrPull"; then + ACR_PULL_FOUND=true + break + fi + + echo " ⏳ AcrPull not yet visible (${ELAPSED}s elapsed). Retrying in ${POLL_INTERVAL}s..." + sleep "$POLL_INTERVAL" + ELAPSED=$((ELAPSED + POLL_INTERVAL)) + done + + if [ "$ACR_PULL_FOUND" = false ]; then + echo "" + echo "❌ ACR readiness check FAILED after ${ACR_RBAC_TIMEOUT}s." + echo " AcrPull role not visible for principal $IDENTITY_PRINCIPAL_ID on $ACR_NAME." + echo "" + echo " To retry: azd deploy --no-prompt" + echo " To increase timeout: ACR_RBAC_TIMEOUT=600 azd deploy --no-prompt" + exit 1 + fi + + echo " ✓ AcrPull role is visible" + + # ── Phase 2: AILZ-only DNS verification ── + if [ "$DEPLOYMENT_MODE" = "ailz-integrated" ]; then + echo "" + echo " Verifying ACR private endpoint DNS resolution (AILZ mode)..." + ACR_LOGIN_SERVER="${ACR_NAME}.azurecr.io" + RESOLVED_IP=$(nslookup "$ACR_LOGIN_SERVER" 2>/dev/null | grep -A1 "privatelink.azurecr.io" | grep "Address:" | awk '{print $2}' || echo "") + + if [ -z "$RESOLVED_IP" ]; then + # Fallback: just check if it resolves to any IP + RESOLVED_IP=$(nslookup "$ACR_LOGIN_SERVER" 2>/dev/null | tail -2 | grep "Address:" | awk '{print $2}' || echo "") + fi + + if [ -z "$RESOLVED_IP" ]; then + echo " ⚠ Could not resolve $ACR_LOGIN_SERVER. ACR private endpoint DNS may not be configured." + echo " Deploy may fail if Container Apps cannot reach the registry." + elif echo "$RESOLVED_IP" | grep -qE "^10\.|^172\.(1[6-9]|2[0-9]|3[01])\.|^192\.168\."; then + echo " ✓ ACR resolves to private IP: $RESOLVED_IP" + else + echo " ⚠ ACR resolves to public IP: $RESOLVED_IP" + echo " In AILZ mode, ACR should resolve to a private endpoint IP." + echo " Verify privatelink.azurecr.io DNS zone is configured correctly." + fi + fi + + # ── Phase 3: Stabilization wait ── + # RBAC can be queryable before ACA can effectively use it. + # A short stabilization wait reduces the chance of a race condition. + if [ "$ACR_RBAC_STABILIZATION" -gt 0 ]; then + echo "" + echo " ⏳ Waiting ${ACR_RBAC_STABILIZATION}s for RBAC stabilization..." + sleep "$ACR_RBAC_STABILIZATION" + echo " ✓ Stabilization complete" + fi + + echo "" + echo " ✓ ACR pull readiness confirmed" + fi +fi + +echo "" echo "==================================================" echo "✓ Pre-deploy checks completed successfully" echo "==================================================" From 8ae95c22e1c65fa161bd7f7388086391ecdd2009 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Mon, 30 Mar 2026 10:24:19 -0400 Subject: [PATCH 26/29] content_flow last tests --- .DS_Store | Bin 10244 -> 10244 bytes .gitignore | 2 +- Analysis/00-full-analysis.md | 1129 +++++++++++ Analysis/01-overview.md | 430 +++++ Analysis/02-architecture-detailed.md | 1920 +++++++++++++++++++ Analysis/03-use_cases_examples.md | 666 +++++++ Analysis/04-ai_lz_options.md | 1224 ++++++++++++ Analysis/05-mini-ailz.md | 1002 ++++++++++ Analysis/06-service-health.md | 825 ++++++++ Analysis/personal-private-configurations.md | 141 ++ 10 files changed, 7338 insertions(+), 1 deletion(-) create mode 100644 Analysis/00-full-analysis.md create mode 100644 Analysis/01-overview.md create mode 100644 Analysis/02-architecture-detailed.md create mode 100644 Analysis/03-use_cases_examples.md create mode 100644 Analysis/04-ai_lz_options.md create mode 100644 Analysis/05-mini-ailz.md create mode 100644 Analysis/06-service-health.md create mode 100644 Analysis/personal-private-configurations.md diff --git a/.DS_Store b/.DS_Store index 5df274e9ed01e2d44968bd4dd626e2db05c65838..3a80f725a55157d3778b683937e2770d1c0c0f0e 100644 GIT binary patch delta 387 zcmZn(XbG6$&uFqSU^hRb$z&dZh0K}ue;0SjMo?n%EkQ1}v;0Y$CH72yC8z>piU_GP2^^SgEi|Q`gTav@k0FsE zhoO?8m?4v)I49jOI5|JJfB^(peYOBe3^lp=E-pzq`AHzT9lowY5_^t2Vpo?!K%FSW z(t-?ROB*Ji7P`b##WeYYxa{OT!h#%j-!;D8-*Ma#Bp{i{P{5Fxn~yLVS$499h%nOv z29UzdQ6gJdH73SRdfT)c>^xSW_wpFh7>XDYk+nlqY&Mg!WZlfJ@QZ~F=o8a*55PwB Ih&*5d0Q)C+X8-^I delta 193 zcmZn(XbG6$&uFkQU^hRb!DJqRh0N*ff|EB0NU-_M^k}%dWAZrxDH!vefHYH)!enm2 zEzFEY6DPkBl$d-=kc& **Single-file reference document for AI agents and developers.** +> This document consolidates all capabilities, architecture, prerequisites, out-of-scope boundaries, and example scenarios for the ContentFlow accelerator. Use it as the authoritative context source before working with this codebase. + +--- + +## Table of Contents + +1. [What is ContentFlow?](#1-what-is-contentflow) +2. [Core Concepts](#2-core-concepts) +3. [Full Capabilities](#3-full-capabilities) + - [3.1 Built-in Executors (37 total)](#31-built-in-executors-37-total) + - [3.2 Pipeline Engine Features](#32-pipeline-engine-features) + - [3.3 Web UI Features](#33-web-ui-features) + - [3.4 API Features](#34-api-features) + - [3.5 Worker Engine Features](#35-worker-engine-features) + - [3.6 Extensibility: Custom Executors](#36-extensibility-custom-executors) +4. [Prerequisites](#4-prerequisites) + - [4.1 Azure Services Required](#41-azure-services-required) + - [4.2 Developer Toolchain](#42-developer-toolchain) + - [4.3 Azure Permissions](#43-azure-permissions) + - [4.4 Service Quotas & Limits](#44-service-quotas--limits) +5. [Final Architecture](#5-final-architecture) + - [5.1 Component Overview](#51-component-overview) + - [5.2 Service Communication Map](#52-service-communication-map) + - [5.3 Data Flow](#53-data-flow) + - [5.4 Task Lifecycle](#54-task-lifecycle) + - [5.5 Cosmos DB Schema](#55-cosmos-db-schema) + - [5.6 Deployment Modes](#56-deployment-modes) + - [5.7 Security Model](#57-security-model) +6. [Out of Scope](#6-out-of-scope) +7. [Example Scenarios](#7-example-scenarios) + - [7.1 Invoice & Receipt Processing](#71-invoice--receipt-processing) + - [7.2 Enterprise Knowledge Base for RAG](#72-enterprise-knowledge-base-for-rag) + - [7.3 Contract Analysis](#73-contract-analysis) + - [7.4 Engineering PDF BOM Extraction](#74-engineering-pdf-bom-extraction) + - [7.5 Compliance & PII Detection](#75-compliance--pii-detection) + - [7.6 Web Content Ingestion](#76-web-content-ingestion) + - [7.7 Multilingual Document Translation](#77-multilingual-document-translation) + - [7.8 Email Attachment Triage](#78-email-attachment-triage) + - [7.9 Spreadsheet Data Pipeline](#79-spreadsheet-data-pipeline) + - [7.10 Knowledge Graph Construction](#710-knowledge-graph-construction) +8. [Included Sample Pipelines](#8-included-sample-pipelines) + +--- + +## 1. What is ContentFlow? + +**ContentFlow** is an open-source, enterprise-grade, cloud-native document and content processing accelerator built on Azure. It transforms unstructured content — PDFs, Word documents, Excel files, PowerPoint presentations, CSV files, web pages, images — into structured, intelligent, actionable data through orchestrated AI-powered pipelines. + +**Primary value proposition**: ContentFlow eliminates the infrastructure plumbing required to build scalable document processing systems on Azure. A developer can go from zero to a production-grade pipeline in hours rather than weeks by combining pre-built executors declaratively via YAML. + +**Repository**: [Azure/contentflow](https://github.com/Azure/contentflow) +**License**: MIT +**Runtime**: Python 3.12+ (backend), TypeScript/React 18 (frontend) + +--- + +## 2. Core Concepts + +### Pipeline +A **pipeline** is the fundamental unit of work. It defines an ordered sequence of executors that content passes through. Pipelines are declared in YAML and stored in Cosmos DB. + +```yaml +name: my-pipeline +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documents + - executor: azure_document_intelligence_extractor + settings: + doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" + - executor: recursive_text_chunker + settings: + chunk_size: 1000 + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + - executor: ai_search_index_output + settings: + index_name: my-index +``` + +### Executor +An **executor** is a self-contained processing unit. Each executor receives a `Content` object, transforms or enriches it, and passes the result to the next step. Executors are composable, configurable via YAML settings, and can run in parallel. + +### Vault +A **vault** is a logical container that associates a document source with a pipeline. When enabled, the Worker service automatically discovers new documents arriving in the vault and queues them for processing. + +### Content Model +The standardized Python object (`Content`) that flows through every pipeline step: + +```python +Content + ├── id: ContentIdentifier # canonical_id, source_type, container, path, filename + ├── data: dict # All executor outputs accumulate here + ├── summary_data: dict # Execution metrics per executor + └── executor_logs: list # Per-step audit trail +``` + +All executor outputs are stored as named fields in `content.data`, enabling downstream executors to reference previous results via dot-notation paths (e.g., `doc_intell_output.tables`). + +--- + +## 3. Full Capabilities + +### 3.1 Built-in Executors (37 total) + +#### Input Executors (3) +| Executor ID | Description | +|---|---| +| `azure_blob_input_discovery` | Discovers files in Azure Blob Storage containers. Supports file extension filtering, prefix filtering, and incremental crawling via checkpoints (processes only new/modified blobs since last run). | +| `azure_blob_content_retriever` | Downloads blob content as bytes or saves to a temp file path for downstream processing. Supports parallel retrieval with configurable concurrency. | +| `content_retriever` | Loads content from local file system or arbitrary byte sources. Primarily used in local development and sample pipelines. | + +#### Document Extraction Executors (6) +| Executor ID | Description | +|---|---| +| `azure_document_intelligence_extractor` | Invokes Azure Document Intelligence (prebuilt-layout, prebuilt-document, prebuilt-read) to extract text, tables, key-value pairs, and figures from any document. Supports markdown or plain text output format, page range selection, locale hints, and additional features like `ocrHighResolution`. | +| `azure_content_understanding_extractor` | Uses Azure Content Understanding for advanced document analysis including prebuilt invoice, receipt, business card, and ID models. | +| `pdf_extractor` | Extracts text, page-level chunks, and embedded images from PDF files using PyMuPDF. No Azure service dependency — runs locally. | +| `word_extractor` | Extracts text, headings, tables, and metadata from `.docx` files using python-docx. | +| `powerpoint_extractor` | Extracts slide content, speaker notes, embedded images, and metadata from `.pptx` files using python-pptx. | +| `excel_extractor` | Extracts sheets, tables, cell values, formulas, images, and metadata from `.xlsx`/`.xlsm` files using openpyxl. Supports first-row-as-header detection, sheet filtering, and table range detection. | +| `csv_extractor` | Parses CSV files with configurable delimiter, quoting, and encoding. | + +#### AI Processing Executors (8) +| Executor ID | Description | +|---|---| +| `azure_openai_agent` | Sends content to Azure OpenAI (GPT-4.1, GPT-4.1-mini, etc.) with a configurable system prompt and user template. The most flexible executor — suitable for summarization, extraction, classification, transformation, and any custom reasoning task. | +| `azure_openai_embeddings` | Generates vector embeddings using Azure OpenAI embedding models (text-embedding-3-small, text-embedding-3-large). Output format compatible with Azure AI Search vector fields. | +| `text_summarizer` | Generates concise summaries with configurable max length and style. Built on `azure_openai_agent`. | +| `entity_extractor` | Extracts named entities (persons, organizations, dates, amounts, locations) from text. Configurable entity types. Built on `azure_openai_agent`. | +| `sentiment_analyser` | Document-level, sentence-level, or aspect-based sentiment analysis with confidence scores. Supports 3-point and 5-point scales and emotion detection. | +| `content_classifier` | Multi-class and multi-label classification with confidence scores and explanations. Accepts custom category lists and descriptions. | +| `pii_detector` | Detects and optionally redacts Personally Identifiable Information (PII) such as names, emails, phone numbers, SSNs, credit cards, addresses. | +| `keyword_extractor` | Extracts top-N keywords and key phrases from text content. | + +#### Transformation Executors (6) +| Executor ID | Description | +|---|---| +| `recursive_text_chunker` | Splits long text into overlapping chunks using recursive character splitting with configurable chunk size, overlap, and separators. | +| `table_row_splitter` | Splits tabular data (list-of-lists, list-of-dicts, CSV string, Word tables, Excel rows) into individual `Content` objects — one per row — enabling per-row parallel processing downstream. | +| `field_mapper` | Renames, remaps, and transforms fields within `content.data` using a declarative mapping configuration. | +| `field_selector` | Includes or excludes specific fields from `content.data` to control what data is passed forward. | +| `language_detector` | Detects the primary language of text content. | +| `content_translator` | Translates content to one or more target languages using Azure OpenAI. | + +#### Output Executors (3) +| Executor ID | Description | +|---|---| +| `azure_blob_output` | Writes processed content (JSON, text, binary) back to Azure Blob Storage. Supports custom path templates and content serialization. | +| `ai_search_index_output` | Indexes processed content into Azure AI Search. Supports vector fields, semantic configuration, and batch indexing. | +| `gptrag_search_index_document_generator` | Generates documents in GPT-RAG-compatible format for Azure AI Search, including chunked content and embeddings for RAG scenarios. | + +#### Control Flow Executors (5) +| Executor ID | Description | +|---|---| +| `subpipeline` | Executes another pipeline as a nested sub-workflow. Enables pipeline composition and reuse. | +| `for_each_content` | Iterates over a list of content items and applies a sub-pipeline to each one. | +| `fan_in_aggregator` | Collects and merges results from parallel branches back into a single content stream. | +| `document_set_initializer` | Initializes a multi-document analysis session for cross-document operations. | +| `document_set_collector` | Collects documents into a set for batch cross-document processing. | + +#### Cross-Document Executors (3) +| Executor ID | Description | +|---|---| +| `cross_document_comparison` | Compares multiple documents side-by-side using AI to identify similarities, differences, and patterns. | +| `cross_document_field_aggregator` | Aggregates specific fields across a document set (e.g., collect all "summary" fields from 50 documents). | +| `cosmos_db_lookup` | Looks up metadata or previously stored results from Cosmos DB during pipeline execution. Useful for enrichment and deduplication. | + +#### Utility Executors (3) +| Executor ID | Description | +|---|---| +| `pass_through` | No-op executor. Useful for pipeline testing and debugging. | +| `web_scraper` | Crawls web pages using Playwright (headless Chromium). Supports CSS selectors, depth control, and JavaScript-rendered pages. | +| `cosmos_db_lookup` | Retrieves enrichment data from Cosmos DB based on a key field in the content. | + +--- + +### 3.2 Pipeline Engine Features + +- **Declarative YAML definition** — pipelines are stored as YAML and parsed at runtime +- **DAG execution** — pipelines can define explicit `execution_sequence` for dependency control +- **Async/await throughout** — all executors are async; built on Python asyncio +- **Parallel execution** — `ParallelExecutor` base class enables `max_concurrent` processing (default: 3 concurrent items) +- **Conditional execution** — any executor supports a `condition` setting (Python expression evaluated against `content.data`) +- **Error handling** — `fail_pipeline_on_error` and `continue_on_error` per executor; up to 3 retries with configurable delay +- **Timeout control** — per-executor `timeout_secs` setting +- **Debug mode** — verbose logging per executor via `debug_mode: true` +- **Nested fields** — all settings support dot-notation for accessing nested data (e.g., `doc_intell_output.tables`) +- **Environment variable substitution** — any setting value can reference env vars with `${VAR_NAME}` syntax +- **Sub-pipelines** — pipelines can invoke other pipelines as steps, enabling workflow composition +- **Parallel fan-out/fan-in** — native support for splitting content into parallel branches and merging results +- **Incremental processing** — vault crawl checkpoints prevent duplicate reprocessing + +--- + +### 3.3 Web UI Features + +**Technology**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn/ui, Radix UI, ReactFlow, Monaco Editor + +| Feature | Description | +|---|---| +| **Visual Pipeline Builder** | Drag-and-drop graphical interface built on ReactFlow. Nodes represent executors; edges represent data flow. | +| **YAML Editor** | Monaco-powered code editor with syntax highlighting for editing pipeline definitions directly. | +| **Template Gallery** | Pre-built pipeline templates for common scenarios, ready to clone and customize. | +| **Vault Management** | Create, edit, enable/disable document vaults. Associate vaults with pipelines. | +| **Execution Dashboard** | Real-time view of pipeline execution status, metrics, and per-executor output. | +| **Executor Catalog Browser** | Browse all 37 executors with descriptions, settings schemas, and usage examples. | +| **Knowledge Graph Viewer** | Visualization of entity-relationship graphs produced by knowledge graph pipelines. | + +**Routes**: +- `/` — Home dashboard +- `/?view=pipeline` — Pipeline builder +- `/?view=vaults` — Vault management +- `/?view=graph` — Knowledge graph visualization +- `/templates` — Template gallery + +--- + +### 3.4 API Features + +**Technology**: FastAPI 0.128, Python 3.12+, Uvicorn, Pydantic v2, Azure SDK +**Port**: 8090 +**Authentication**: Azure Managed Identity (no API keys in production) + +| Endpoint group | Operations | +|---|---| +| `GET /api/health` | System health check — verifies Cosmos DB, Blob, Queue, App Config connectivity | +| `GET/POST/PUT/DELETE /api/pipelines` | Full CRUD for pipeline definitions; trigger manual execution | +| `GET/POST/PUT/DELETE /api/vaults` | Vault management including enable/disable | +| `GET /api/vaults/{id}/executions` | Vault execution history and status | +| `GET /api/executors` | Browse executor catalog with full settings schemas | +| `GET /docs` | Interactive Swagger UI | +| `GET /redoc` | ReDoc documentation | + +--- + +### 3.5 Worker Engine Features + +**Technology**: Python 3.12+, multiprocessing, Azure Storage Queue +**Port**: 8099 (health API) + +| Feature | Description | +|---|---| +| **Multi-process architecture** | N Source Workers + M Processing Workers run as independent OS processes | +| **Source Workers** | Poll enabled vaults every 5 minutes (configurable), discover new content, enqueue tasks | +| **Processing Workers** | Poll Azure Storage Queue, execute full pipelines on content items | +| **Incremental crawling** | Checkpoints stored in Cosmos DB prevent reprocessing already-seen documents | +| **Distributed locking** | Lock TTL prevents duplicate processing when multiple workers poll simultaneously | +| **Auto-restart** | Monitor loop detects crashed worker processes and replaces them automatically | +| **Graceful shutdown** | SIGINT/SIGTERM handled via shared `multiprocessing.Event`; workers finish current task before stopping | +| **Configurable parallelism** | `NUM_PROCESSING_WORKERS` and `NUM_SOURCE_WORKERS` env vars | +| **Task retries** | Up to 3 retries per task with configurable delay | + +--- + +### 3.6 Extensibility: Custom Executors + +ContentFlow is designed for extension. New executors can be created by subclassing one of three base classes: + +| Base Class | Use case | +|---|---| +| `BaseExecutor` | Single content item or list processing; implement `process_input()` | +| `ParallelExecutor` | Automatic parallel processing; implement `process_content_item()` once; framework handles concurrency | +| `InputExecutor` | Input source discovery; implement `crawl()` to yield new content items | + +After implementing the class: +1. Register it in `executor_catalog.yaml` with its `id`, `module_path`, `class_name`, tags, and `settings_schema` +2. Import it in `contentflow/executors/__init__.py` + +Custom executors work identically to built-in ones — they appear in the Web UI, accept YAML configuration, support all base class features (debug mode, conditions, retries, timeouts), and integrate with the full pipeline engine. + +--- + +## 4. Prerequisites + +### 4.1 Azure Services Required + +| Service | SKU (Basic mode) | SKU (AILZ mode) | Purpose | +|---|---|---|---| +| **Azure Container Apps Environment** | Consumption | Consumption (Internal) | Hosts API, Worker, Web containers | +| **Azure Cosmos DB** | Serverless | Serverless + Private Endpoint | Pipeline definitions, execution state, vault metadata | +| **Azure Blob Storage** | Standard LRS, Hot | Standard LRS + Private Endpoint | Document storage, processed outputs | +| **Azure Storage Queue** | Standard | Standard + Private Endpoint | Task queue between Source and Processing Workers | +| **Azure App Configuration** | Standard | Standard + Private Endpoint | Centralized runtime configuration | +| **Azure Application Insights** | Pay-per-use | Pay-per-use | Distributed tracing, monitoring | +| **Azure Log Analytics** | PerGB2018 | PerGB2018 (shared) | Log aggregation | +| **Azure Container Registry** | Standard | **Premium** (required for Private Endpoint) | Container image registry | +| **Azure Managed Identity** | User-assigned | User-assigned | Passwordless authentication between all services | + +**Optional Azure services** (required for specific executors): + +| Service | Required by executor(s) | +|---|---| +| **Azure AI Foundry / OpenAI** | `azure_openai_agent`, `azure_openai_embeddings`, `text_summarizer`, `entity_extractor`, `sentiment_analyser`, `content_classifier`, `pii_detector`, `keyword_extractor`, `content_translator` | +| **Azure Document Intelligence** | `azure_document_intelligence_extractor` | +| **Azure Content Understanding** | `azure_content_understanding_extractor` | +| **Azure AI Search** | `ai_search_index_output`, `gptrag_search_index_document_generator` | + +### 4.2 Developer Toolchain + +```bash +# Required for local development +python --version # 3.12+ +node --version # 18+ +npm --version # 9+ +docker --version # Any recent version +az --version # Azure CLI 2.60+ +azd version # Azure Developer CLI 1.5+ + +# Required for deployment +git --version # Any recent version +``` + +**Python dependencies** (installed from `requirements.txt` per service): +- `fastapi>=0.128.0`, `uvicorn>=0.40.0`, `pydantic>=2.12` +- `azure-identity>=1.25.1`, `azure-cosmos>=4.14.3` +- `azure-storage-blob>=12.27.1`, `azure-storage-queue>=12.14.1` +- `azure-appconfiguration-provider>=2.3.1` +- `azure-ai-documentintelligence` (for Document Intelligence) +- `openai` (for Azure OpenAI) +- `pymupdf` (for PDF extraction) +- `python-docx`, `python-pptx`, `openpyxl` (Office documents) +- `playwright` (for web scraping — requires browser install) +- `agent-framework` (Microsoft Agent Framework — pipeline orchestration) + +### 4.3 Azure Permissions + +| Permission | Scope | Required for | +|---|---|---| +| `Contributor` | ContentFlow Resource Group | Deploy all resources | +| `User Access Administrator` | ContentFlow Resource Group | Assign RBAC to Managed Identity | +| `Network Contributor` | VNet Resource Group | Create Private Endpoints (AILZ mode only) | +| `Private DNS Zone Contributor` | DNS Zone Resource Group | Register PE records (AILZ mode only) | + +**RBAC roles assigned automatically to the Managed Identity during deployment**: +- `Storage Blob Data Contributor` on Storage Account +- `Storage Queue Data Contributor` on Storage Account +- `Cosmos DB Built-in Data Contributor` on Cosmos DB +- `App Configuration Data Reader` on App Configuration +- `AcrPull` on Container Registry +- `Cognitive Services User` on AI Services (when applicable) + +### 4.4 Service Quotas & Limits + +| Limit | Default | Notes | +|---|---|---| +| Container Apps replicas | 1–2 per service | Auto-scales based on load | +| Worker processes | Configurable | `NUM_PROCESSING_WORKERS` env var | +| Queue message visibility timeout | 300 seconds | Task must complete within 5 minutes or is retried | +| Task max retries | 3 | Configurable via pipeline settings | +| Task timeout | 600 seconds | 10 minutes per pipeline execution | +| Cosmos DB throughput | Serverless (on-demand) | No pre-provisioned RU/s needed | +| Azure OpenAI TPM | Varies by deployment | Should be sized for expected concurrency | + +--- + +## 5. Final Architecture + +### 5.1 Component Overview + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ AZURE CONTAINER APPS ENVIRONMENT │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ +│ │ contentflow- │ │ contentflow- │ │ contentflow- │ │ +│ │ web │ │ api │ │ worker │ │ +│ │ │ │ │ │ │ │ +│ │ React 18 + │──▶│ FastAPI │ │ Multi-process │ │ +│ │ TypeScript │ │ Python 3.12+ │ │ Python 3.12+ │ │ +│ │ Port: 80 │ │ Port: 8090 │ │ Port: 8099 │ │ +│ │ │ │ │ │ (health only) │ │ +│ │ Pipeline │ │ Routers: │ │ │ │ +│ │ Builder UI │ │ /pipelines │ │ Source Workers │ │ +│ │ Vault Manager │ │ /vaults │ │ (crawl vaults) │ │ +│ │ Exec Dashboard │ │ /executors │ │ │ │ +│ │ Template Gall. │ │ /health │ │ Processing │ │ +│ └─────────────────┘ └────────┬────────┘ │ Workers │ │ +│ │ │ (run pipelines) │ │ +└──────────────────────────────────┼────────────┴──────────────────┘ │ + │ │ + ┌─────────────────────────┼────────────────────┼──────────────┐ + │ ▼ ▼ │ + │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ + │ │ Cosmos DB │ │ Blob Storage │ │ Storage Queue │ │ + │ │ (Serverless)│ │ (LRS, Hot) │ │ (processing │ │ + │ │ │ │ │ │ tasks) │ │ + │ │ pipelines │ │ vaults/ │ │ │ │ + │ │ vaults │ │ processed/ │ │ │ │ + │ │ executions │ │ outputs/ │ │ │ │ + │ │ checkpoints │ │ │ │ │ │ + │ └──────────────┘ └──────────────┘ └─────────────────┘ │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ + │ │ App Config │ │ App Insights│ │ Container │ │ + │ │ (Standard) │ │ + Log Anal. │ │ Registry │ │ + │ └──────────────┘ └──────────────┘ └─────────────────┘ │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ + │ │ AI Foundry │ │ Document │ │ Azure AI │ │ + │ │ (OpenAI) │ │ Intelligence│ │ Search │ │ + │ │ (optional) │ │ (optional) │ │ (optional) │ │ + │ └──────────────┘ └──────────────┘ └─────────────────┘ │ + │ AZURE MANAGED SERVICES │ + └──────────────────────────────────────────────────────────────┘ +``` + +### 5.2 Service Communication Map + +| From | To | Protocol | Auth | +|---|---|---|---| +| Web UI | ContentFlow API | HTTPS REST | None (same environment) | +| API | Cosmos DB | Azure SDK over HTTPS | Managed Identity | +| API | Blob Storage | Azure SDK over HTTPS | Managed Identity | +| API | Azure App Configuration | Azure SDK over HTTPS | Managed Identity | +| Worker (Source) | Cosmos DB | Azure SDK over HTTPS | Managed Identity | +| Worker (Source) | Storage Queue | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Storage Queue | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Cosmos DB | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Blob Storage | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Azure OpenAI | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Document Intelligence | Azure SDK over HTTPS | Managed Identity | +| Worker (Processing) | Azure AI Search | Azure SDK over HTTPS | Managed Identity | + +### 5.3 Data Flow + +``` +1. CONTENT DISCOVERY + Source Worker + │ + ├── Query Cosmos DB → enabled vaults + ├── For each vault → read associated pipeline + ├── Run input executor (e.g., azure_blob_input_discovery) + ├── Discover new files since last checkpoint + ├── Create ContentProcessingTask per file + ├── Enqueue tasks → Azure Storage Queue + └── Save new crawl checkpoint → Cosmos DB + +2. CONTENT PROCESSING + Processing Worker + │ + ├── Poll Storage Queue (up to 32 messages) + ├── Deserialize ContentProcessingTask + ├── Acquire distributed lock (prevent duplicate processing) + ├── Load pipeline YAML from Cosmos DB + ├── For each pipeline step (skipping input executors): + │ ├── Instantiate executor + │ ├── Check condition (if set) + │ ├── Call executor.process_input(content) + │ └── Accumulate results in content.data + ├── Update execution status → Cosmos DB (COMPLETED / FAILED) + ├── Write output → Blob Storage (if configured) + └── Delete message from queue + +3. OUTPUT + Configured output executors write to: + ├── Azure Blob Storage (JSON, text, binary) + ├── Azure AI Search (indexed documents) + └── Any custom destination via custom executor +``` + +### 5.4 Task Lifecycle + +``` +[PENDING] ← Task created and enqueued + │ + ▼ +[RUNNING] ← Processing Worker picks up the task + │ + ├──── Success ──→ [COMPLETED] + │ │ + │ Store results in Cosmos DB + Blob + │ + ├──── Failure ──→ [FAILED] + │ │ + │ ├── retry_count < max_retries (3)? + │ │ └── Yes → re-enqueue → [PENDING] + │ └── No → [FAILED] (permanent) + │ + └──── Cancelled ─→ [CANCELLED] +``` + +### 5.5 Cosmos DB Schema + +**Database**: `contentflow-db` + +| Container | Partition Key | Contents | +|---|---|---| +| `pipelines` | `/id` | Pipeline YAML definitions, graph nodes/edges, metadata | +| `vaults` | `/id` | Vault configurations, associated pipeline IDs | +| `vault_executions` | `/vault_id` | Per-document execution records with status and outputs | +| `pipeline_executions` | `/pipeline_id` | Direct pipeline execution records | +| `vault_crawl_checkpoints` | `/vault_id` | Last crawl timestamp per vault+executor | +| `executor_locks` | `/task_id` | Distributed locks for deduplication | +| `executor_catalog` | `/id` | Cached executor catalog (loaded from YAML at startup) | + +### 5.6 Deployment Modes + +#### Basic Mode (Public Endpoints) +All Azure resources expose public endpoints. Suitable for development, demos, and non-regulated environments. + +``` +Internet → Container Apps (external ingress) → Azure Services (public endpoints) +``` + +**Deploy**: `azd up` (one command) + +#### AILZ-Integrated Mode (Private Endpoints) +All Azure resources have `publicNetworkAccess: Disabled`. Traffic flows only through Private Endpoints inside a VNet. Suitable for enterprise, regulated environments, and AI Landing Zone integration. + +``` +JumpBox/VPN → VNet → Container Apps (internal LB) → Azure Services (Private Endpoints) +``` + +**Differences from Basic**: +- Container Apps Environment: `internal: true` (no public ingress) +- All services: `publicNetworkAccess: Disabled` +- Network ACLs: `defaultAction: Deny`, `bypass: AzureServices` +- Container Registry: must be **Premium** SKU (required for Private Endpoint support) +- Requires pre-existing VNet with subnets: `pe-subnet` and `aca-env-subnet` +- Requires 6 Private DNS Zones linked to the VNet: + - `privatelink.blob.core.windows.net` + - `privatelink.queue.core.windows.net` + - `privatelink.documents.azure.com` + - `privatelink.azconfig.io` + - `privatelink.azurecr.io` + - `privatelink.cognitiveservices.azure.com` + +**Deploy**: `azd up` with `ailzMode=ailz-integrated` parameter and pre-existing network resource IDs. + +### 5.7 Security Model + +| Principle | Implementation | +|---|---| +| **Zero-Trust** | No passwords, no connection strings. All service-to-service communication uses Managed Identity. | +| **Passwordless** | `ChainedTokenCredential` (Managed Identity → `DefaultAzureCredential`) used across all Azure SDK calls. | +| **Least Privilege** | Each service's Managed Identity is assigned only the RBAC roles it needs on each resource. | +| **Network Isolation** | AILZ mode enforces private networking; Basic mode exposes public endpoints with HTTPS. | +| **Secrets Management** | All secrets are either environment variables injected at deployment time or resolved from Azure App Configuration. No secrets in code or Git. | +| **Centralized Config** | Azure App Configuration is the single source of truth for runtime settings; local `.env` is for development only. | +| **CORS** | API configures `allow_origins: ["*"]` (suitable for internal deployments; restrict in production). | + +--- + +## 6. Out of Scope + +The following capabilities are **not included** in ContentFlow out-of-the-box and would require custom executor development, additional Azure services, or integration work: + +| Capability | Notes | +|---|---| +| **SharePoint read/write connector** | `ContentIdentifier.source_type` mentions "sharepoint" but no executor or connector exists. Would require Microsoft Graph API integration as a custom executor. | +| **OneDrive connector** | Same as SharePoint — no native executor. The Web UI shows it as a planned input type. | +| **Excel/XLSX writer** | `ExcelExtractorExecutor` reads Excel but there is no executor to **write** formatted `.xlsx` files (e.g., with conditional formatting). Requires custom executor using openpyxl. | +| **Confidence score per BOM cell (Document Intelligence)** | The `extract_tables()` method in `DocumentIntelligenceConnector` returns cell content and position but does not expose `cell.confidence` from the SDK response. Requires a code extension to the connector. | +| **Email (Exchange/Outlook) connector** | No native email source executor. Would require Microsoft Graph API integration. | +| **Relational database connector** (SQL, PostgreSQL) | No native DB input executor. Would require a custom `InputExecutor` subclass. | +| **Real-time / streaming processing** | ContentFlow is a batch/async pipeline system. It does not support real-time streaming ingestion (e.g., Kafka, Event Hub consumer). | +| **Chat interface** | No conversational UI or chat bot. ContentFlow processes documents, it does not serve a chat endpoint. Integration with Copilot Studio or Azure Bot Service is out of scope. | +| **Azure AI Search index schema management** | The `ai_search_index_output` executor writes to an existing index but does not create or manage index schemas. Index schema provisioning must happen separately. | +| **Custom Document Intelligence model training** | ContentFlow uses pre-built or previously trained models. Training custom Document Intelligence models is outside the accelerator's scope. | +| **Dynamics 365 / SAP integration** | No native connectors for line-of-business systems. Outputs to Blob or AI Search can be consumed by downstream integrations. | +| **Multi-tenant isolation** | All pipelines and vaults within a deployment share the same Cosmos DB and Blob Storage. There is no tenant-level data separation built in. | +| **Role-based access control in the UI** | The Web UI has no authentication or authorization layer. It is intended for internal use by platform operators. | +| **Built-in pipeline scheduling** | Vaults trigger continuous background polling; there is no cron-style scheduler. Time-based triggers must be implemented externally (e.g., Logic Apps, Azure Functions). | +| **Video or audio processing** | No native executors for audio transcription or video analysis. These would require Azure Speech or Azure Video Indexer custom executors. | + +--- + +## 7. Example Scenarios + +### 7.1 Invoice & Receipt Processing + +**Goal**: Extract structured data from incoming invoices PDFs and images; classify by document type; flag anomalies; output to Blob Storage. + +**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_content_understanding_extractor` (prebuilt-invoice) → `content_classifier` → `field_mapper` → `azure_blob_output` + +```yaml +name: invoice-processing +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: invoices-incoming + file_extensions: [".pdf", ".png", ".jpg"] + + - executor: azure_blob_content_retriever + + - executor: azure_content_understanding_extractor + settings: + model_id: prebuilt-invoice + output_field: invoice_data + + - executor: content_classifier + settings: + categories: ["invoice", "receipt", "purchase_order", "credit_note"] + include_confidence: true + + - executor: field_mapper + settings: + mappings: + invoice_data.VendorName: vendor_name + invoice_data.InvoiceTotal.amount: total_amount + invoice_data.InvoiceDate: invoice_date + + - executor: azure_blob_output + settings: + blob_container_name: invoices-processed + output_format: json +``` + +--- + +### 7.2 Enterprise Knowledge Base for RAG + +**Goal**: Process a mixed-format document library (PDF, Word, Excel), chunk text, generate embeddings, and index in Azure AI Search to power a RAG chatbot. + +**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_document_intelligence_extractor` → `recursive_text_chunker` → `azure_openai_embeddings` → `gptrag_search_index_document_generator` → `ai_search_index_output` + +```yaml +name: knowledge-base-ingestion +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: company-documents + file_extensions: [".pdf", ".docx", ".xlsx"] + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + settings: + doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" + model_id: prebuilt-layout + extract_text: true + extract_tables: true + + - executor: recursive_text_chunker + settings: + chunk_size: 800 + chunk_overlap: 150 + input_field: doc_intell_output.text + + - executor: azure_openai_embeddings + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: text-embedding-3-small + input_field: chunks + + - executor: gptrag_search_index_document_generator + settings: + index_name: company-knowledge-base + + - executor: ai_search_index_output + settings: + endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" + index_name: company-knowledge-base +``` + +--- + +### 7.3 Contract Analysis + +**Goal**: Extract obligations, parties, dates, monetary values, and risk clauses from legal contracts. Generate an executive summary per document. + +**Key executors**: `pdf_extractor` → `entity_extractor` → `azure_openai_agent` → `text_summarizer` → `azure_blob_output` + +```yaml +name: contract-analysis +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: contracts + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + settings: + extract_text: true + extract_pages: true + + - executor: entity_extractor + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + entity_types: ["person", "organization", "date", "monetary_amount", "jurisdiction"] + output_field: entities + + - executor: azure_openai_agent + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + instructions: | + You are a legal contract analyst. Analyze the contract and extract: + 1. Key obligations per party + 2. Payment terms and amounts + 3. Termination clauses + 4. Governing law and jurisdiction + 5. Risk flags (unusual or one-sided clauses) + Respond in structured JSON. + input_field: pdf_output.text + output_field: contract_analysis + + - executor: text_summarizer + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + max_length: 400 + style: executive summary for legal review + input_field: pdf_output.text + output_field: summary + + - executor: azure_blob_output + settings: + blob_container_name: contracts-analyzed +``` + +--- + +### 7.4 Engineering PDF BOM Extraction + +**Goal**: Extract Bill of Materials (BOM) tables from engineering PDFs using Azure Document Intelligence, flag low-confidence line items (<90%), and write output to Azure Blob Storage. *(See fit assessment in previous analysis for implementation gap details.)* + +**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_document_intelligence_extractor` → `table_row_splitter` → `field_mapper` → [custom: `bom_excel_writer`] → [custom: `sharepoint_output`] + +```yaml +name: engineering-bom-extraction +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: engineering-pdfs + file_extensions: [".pdf"] + + - executor: azure_blob_content_retriever + settings: + use_temp_file_for_content: true + + - executor: azure_document_intelligence_extractor + settings: + doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" + model_id: prebuilt-layout + extract_tables: true + extract_text: false + output_field: doc_intell_output + + - executor: table_row_splitter + settings: + table_field: doc_intell_output.tables.0 + table_format: auto + has_header: true + skip_empty_rows: true + include_row_index: true + + - executor: field_mapper + settings: + mappings: + row_data.Item: item_number + row_data.Description: description + row_data.Qty: quantity + row_data.Unit: unit + row_data.Part_Number: part_number + + # Steps 6 and 7 require custom executors (not built-in): + # - bom_excel_writer: writes .xlsx with red fill on rows where confidence < 0.90 + # - sharepoint_output: writes file back to SharePoint via Microsoft Graph API + + - executor: azure_blob_output + settings: + blob_container_name: bom-output + output_format: json +``` + +**Custom executor gaps required for full Phase 1**: +- `bom_excel_writer` — writes formatted Excel with `openpyxl`, applies `PatternFill(red)` to rows where confidence < 0.90 +- `sharepoint_output` — uploads file to SharePoint document library via Microsoft Graph API +- Extend `DocumentIntelligenceConnector.extract_tables()` to expose `cell.confidence` from the SDK response + +--- + +### 7.5 Compliance & PII Detection + +**Goal**: Scan documents for PII, classify by sensitivity level, and generate compliance risk reports. + +**Key executors**: `pdf_extractor` / `word_extractor` → `pii_detector` → `content_classifier` → `azure_openai_agent` → `azure_blob_output` + +```yaml +name: compliance-scan +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documents-review + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + settings: + extract_text: true + + - executor: pii_detector + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + pii_types: ["name", "email", "phone", "ssn", "credit_card", "address", "dob"] + redact: false + output_field: pii_results + + - executor: content_classifier + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + categories: ["public", "internal", "confidential", "restricted"] + include_confidence: true + output_field: classification + + - executor: azure_openai_agent + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + instructions: | + Based on PII findings and document classification, assess regulatory exposure: + - GDPR, HIPAA, PCI-DSS applicability + - Risk level: low / medium / high / critical + - Required remediation actions + output_field: compliance_report + + - executor: azure_blob_output + settings: + blob_container_name: compliance-reports +``` + +--- + +### 7.6 Web Content Ingestion + +**Goal**: Crawl product documentation or support sites, chunk content, generate embeddings, and index in Azure AI Search for a RAG chatbot. + +**Key executors**: `web_scraper` → `recursive_text_chunker` → `language_detector` → `azure_openai_embeddings` → `ai_search_index_output` + +```yaml +name: web-content-ingestion +steps: + - executor: web_scraper + settings: + urls: + - "https://docs.company.com" + - "https://support.company.com" + max_depth: 3 + selectors: ["article", "main", ".content"] + output_field: scraped_content + + - executor: recursive_text_chunker + settings: + chunk_size: 1500 + chunk_overlap: 200 + input_field: scraped_content.text + + - executor: language_detector + settings: + input_field: scraped_content.text + output_field: detected_language + + - executor: azure_openai_embeddings + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: text-embedding-3-small + + - executor: ai_search_index_output + settings: + endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" + index_name: web-content-rag +``` + +--- + +### 7.7 Multilingual Document Translation + +**Goal**: Detect language of incoming documents, translate to target languages, and index translated content. + +**Key executors**: `pdf_extractor` → `language_detector` → `recursive_text_chunker` → `content_translator` → `azure_openai_embeddings` → `ai_search_index_output` + +```yaml +name: multilingual-translation +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: original-content + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + settings: + extract_text: true + + - executor: language_detector + settings: + input_field: pdf_output.text + output_field: source_language + + - executor: recursive_text_chunker + settings: + chunk_size: 2000 + input_field: pdf_output.text + + - executor: content_translator + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + target_languages: ["en", "es", "fr", "de", "pt"] + output_field: translations + + - executor: azure_openai_embeddings + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: text-embedding-3-small + + - executor: ai_search_index_output + settings: + endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" + index_name: multilingual-content +``` + +--- + +### 7.8 Email Attachment Triage + +**Goal**: Process email attachments, detect sentiment, extract action items and deadlines, classify by category, and route to the appropriate team queue. + +**Key executors**: `pdf_extractor` → `sentiment_analyser` → `entity_extractor` → `azure_openai_agent` → `content_classifier` → `azure_blob_output` + +```yaml +name: email-triage +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: email-attachments + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + settings: + extract_text: true + + - executor: sentiment_analyser + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + granularity: document + include_confidence: true + output_field: sentiment + + - executor: entity_extractor + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + entity_types: ["person", "date", "organization", "action_item"] + output_field: entities + + - executor: azure_openai_agent + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + instructions: | + Extract from this email: + 1. Action items and who owns them + 2. Deadlines mentioned + 3. Urgency level: low / medium / high / critical + 4. Subject matter summary in one sentence + output_field: triage_result + + - executor: content_classifier + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1-mini + categories: ["technical_support", "billing", "complaint", "legal", "general_inquiry"] + output_field: category + + - executor: azure_blob_output + settings: + blob_container_name: email-triaged +``` + +--- + +### 7.9 Spreadsheet Data Pipeline + +**Goal**: Process Excel files row by row, normalize field names, validate data, and export cleaned records to Blob Storage as JSON. + +**Key executors**: `content_retriever` → `excel_extractor` → `table_row_splitter` → `field_mapper` → `field_selector` → `azure_blob_output` + +```yaml +name: spreadsheet-pipeline +steps: + - executor: content_retriever + settings: + use_temp_file_for_content: true + + - executor: excel_extractor + settings: + extract_text: false + extract_sheets: true + extract_tables: true + first_row_as_header: true + skip_hidden_sheets: true + output_field: excel_output + + - executor: table_row_splitter + settings: + table_field: excel_output.sheets.0.table + table_format: auto + has_header: true + skip_empty_rows: true + include_row_index: true + + - executor: field_mapper + settings: + mappings: + row_data.CustomerID: customer_id + row_data.CustomerName: name + row_data.EmailAddress: email + row_data.PurchaseAmount: amount + + - executor: field_selector + settings: + include_fields: ["customer_id", "name", "email", "amount", "row_index"] + + - executor: azure_blob_output + settings: + blob_container_name: processed-data + output_format: json +``` + +--- + +### 7.10 Knowledge Graph Construction + +**Goal**: Extract entities and relationships from a corporate document corpus and build a structured knowledge graph for organizational intelligence. + +**Key executors**: `azure_document_intelligence_extractor` → `recursive_text_chunker` → `entity_extractor` → `azure_openai_agent` → `document_set_initializer` → `cross_document_comparison` → `azure_blob_output` + +```yaml +name: knowledge-graph +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: corporate-documents + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + settings: + doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" + model_id: prebuilt-layout + extract_text: true + + - executor: recursive_text_chunker + settings: + chunk_size: 2000 + chunk_overlap: 100 + input_field: doc_intell_output.text + + - executor: entity_extractor + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + entity_types: ["person", "organization", "product", "location", "project", "technology"] + output_field: entities + + - executor: azure_openai_agent + settings: + endpoint: "${AZURE_OPENAI_ENDPOINT}" + deployment_name: gpt-4.1 + instructions: | + From the extracted entities, identify relationships such as: + works_at, manages, reports_to, located_in, provides, uses, competes_with + Return as structured JSON with relationship triples. + output_field: relationships + + - executor: azure_blob_output + settings: + blob_container_name: knowledge-graphs + output_format: json +``` + +--- + +## 8. Included Sample Pipelines + +The repository includes 28+ ready-to-run sample pipelines in `contentflow-lib/samples/`: + +| Folder | Sample Name | What it demonstrates | +|---|---|---| +| `01-simple/` | Simple pipeline | Minimal pipeline: content retrieval + Document Intelligence | +| `02-batch-processing/` | Batch processing | Processing multiple documents concurrently | +| `03-pdf-extractor_chunker/` | PDF + Chunking | PyMuPDF extraction + recursive text chunker | +| `04-word-extractor/` | Word extractor | `.docx` content extraction with metadata | +| `05-powerpoint-extractor/` | PowerPoint extractor | `.pptx` slide-by-slide extraction | +| `06-ai-analysis/` | AI analysis | GPT-4 analysis with configurable prompts | +| `07-embeddings/` | Embeddings | Azure OpenAI embedding generation | +| `08-content-understanding/` | Content Understanding | Azure Content Understanding prebuilt models | +| `09-blob-input/` | Blob input | Full blob discovery + retrieval + Document Intelligence | +| `10-table-row-splitter/` | Table row splitter | Splitting tables into per-row content items | +| `11-excel-extractor/` | Excel extractor | Full Excel workbook extraction | +| `12-field-transformation/` | Field transformation | Field mapping, selection, and normalization | +| `13-blob-output-sample/` | Blob output | Writing processed results to Blob Storage | +| `14-gpt-rag-ingestion/` | GPT-RAG ingestion | Complete end-to-end RAG knowledge base pipeline | +| `15-document-analysis/` | Document analysis | Full Document Intelligence analysis workflow | +| `16-spreadsheet-pipeline/` | Spreadsheet pipeline | Excel ingestion → row splitting → field normalization | +| `17-knowledge-graph/` | Knowledge graph | Entity + relationship extraction for graph construction | +| `18-web-scraping/` | Web scraping | Playwright-based crawling and content extraction | +| `19-sub-pipelines/` | Sub-pipelines | Pipeline composition and nested workflow execution | +| `20-document-set-static/` | Document set (static) | Fixed multi-document cross-analysis | +| `21-document-set-comparison/` | Document comparison | Side-by-side AI comparison of two documents | +| `22-document-set-dynamic/` | Document set (dynamic) | Dynamically assembled document sets | +| `23-inline-document-set/` | Inline document set | Document sets defined inline in the pipeline | +| `27-subpipeline-processing/` | Subpipeline processing | Advanced sub-pipeline patterns | +| `28-advanced-batch/` | Advanced batch | Complex batch processing with error handling | +| `32-parallel-processing/` | Parallel processing | Fan-out/fan-in with concurrent branches | +| `44-conditional-routing/` | Conditional routing | If/else branching based on content properties | + +**Parallel workflow patterns** (documented in `PARALLEL_WORKFLOWS_GUIDE.md`): +- `fan_out_fan_in.yaml` — Split into parallel branches, merge results +- `split_merge.yaml` — Split content list, process in parallel, collect results +- `batch_subworkflow_example.yaml` — Batch processing with nested subworkflows +- `conditional_routing.yaml` — Content-aware routing based on classification results + +--- + +*Document version: March 25, 2026 — Based on ContentFlow repository branch `local-documentation`* diff --git a/Analysis/01-overview.md b/Analysis/01-overview.md new file mode 100644 index 0000000..9a13436 --- /dev/null +++ b/Analysis/01-overview.md @@ -0,0 +1,430 @@ +# ContentFlow — Guía General de la Solución + +> **Plataforma inteligente de procesamiento de documentos y contenido construida sobre Azure.** + +--- + +## Índice + +- [1. ¿Qué es ContentFlow?](#1-qué-es-contentflow) +- [2. ¿Qué problema resuelve?](#2-qué-problema-resuelve) +- [3. Conceptos clave](#3-conceptos-clave) + - [3.1 Pipelines](#31-pipelines) + - [3.2 Executors (Ejecutores)](#32-executors-ejecutores) + - [3.3 Vaults (Bóvedas de documentos)](#33-vaults-bóvedas-de-documentos) + - [3.4 Modelo de Contenido](#34-modelo-de-contenido) +- [4. Componentes principales](#4-componentes-principales) + - [4.1 Interfaz Web (contentflow-web)](#41-interfaz-web-contentflow-web) + - [4.2 API REST (contentflow-api)](#42-api-rest-contentflow-api) + - [4.3 Workers de procesamiento (contentflow-worker)](#43-workers-de-procesamiento-contentflow-worker) + - [4.4 Librería de procesamiento (contentflow-lib)](#44-librería-de-procesamiento-contentflow-lib) + - [4.5 Infraestructura (infra)](#45-infraestructura-infra) +- [5. ¿Cómo funciona el flujo completo?](#5-cómo-funciona-el-flujo-completo) + - [5.1 Paso a paso: De un documento a datos inteligentes](#51-paso-a-paso-de-un-documento-a-datos-inteligentes) + - [5.2 Diagrama de flujo general](#52-diagrama-de-flujo-general) +- [6. Servicios de Azure utilizados](#6-servicios-de-azure-utilizados) +- [7. Modos de despliegue](#7-modos-de-despliegue) + - [7.1 Modo Básico](#71-modo-básico) + - [7.2 Modo AILZ (AI Landing Zone)](#72-modo-ailz-ai-landing-zone) +- [8. Ejemplo práctico: Pipeline de documentos PDF](#8-ejemplo-práctico-pipeline-de-documentos-pdf) +- [9. Resumen ejecutivo](#9-resumen-ejecutivo) + +--- + +## 1. ¿Qué es ContentFlow? + +**ContentFlow** es una plataforma empresarial cloud-native diseñada para transformar contenido no estructurado (PDFs, documentos Word, hojas de Excel, presentaciones PowerPoint, páginas web, etc.) en **datos inteligentes y accionables** mediante flujos de trabajo automatizados impulsados por servicios de inteligencia artificial de Azure. + +Piensa en ContentFlow como una **fábrica de procesamiento de documentos**: introduces documentos crudos por un extremo y, tras pasar por una serie de estaciones de procesamiento configurables (extracción, análisis, enriquecimiento, etc.), obtienes datos estructurados, indexados y listos para consumir. + +--- + +## 2. ¿Qué problema resuelve? + +Las organizaciones enfrentan desafíos comunes con su contenido: + +| Desafío | Cómo lo resuelve ContentFlow | +|---------|------------------------------| +| Documentos en múltiples formatos (PDF, Word, Excel...) | **Ejecutores de extracción** especializados para cada formato | +| Procesamiento manual y lento | **Pipelines automatizados** que procesan contenido sin intervención | +| Necesidad de análisis inteligente | **Integración con Azure AI** (GPT-4, Document Intelligence, embeddings) | +| Escalabilidad limitada | **Arquitectura distribuida** con workers paralelos y colas de mensajes | +| Dificultad para configurar flujos | **Editor visual** drag-and-drop + definición YAML | +| Seguridad empresarial | **Identidad administrada** (Managed Identity) sin contraseñas | + +--- + +## 3. Conceptos clave + +### 3.1 Pipelines + +Un **pipeline** es la unidad fundamental de trabajo en ContentFlow. Define una secuencia de pasos (ejecutores) que el contenido debe recorrer para ser procesado. Se definen en formato YAML: + +```yaml +name: procesar-pdfs +steps: + - executor: azure_blob_input_discovery # Descubre documentos + settings: + blob_container_name: documentos + + - executor: pdf_extractor # Extrae texto del PDF + + - executor: recursive_text_chunker # Divide en fragmentos + settings: + chunk_size: 1000 + + - executor: azure_openai_embeddings # Genera vectores (embeddings) + settings: + model: text-embedding-3-small + + - executor: azure_blob_output # Guarda resultados + settings: + blob_container_name: procesados +``` + +### 3.2 Executors (Ejecutores) + +Los **ejecutores** son las unidades de procesamiento individuales dentro de un pipeline. Cada ejecutor realiza una tarea específica. ContentFlow incluye **más de 35 ejecutores** organizados en categorías: + +| Categoría | Descripción | Ejemplos | +|-----------|-------------|----------| +| **Entrada** | Descubren contenido desde orígenes de datos | Blob Storage, sistema de archivos | +| **Extracción** | Parsean documentos y extraen texto | PDF, Word, Excel, PowerPoint, CSV | +| **Procesamiento IA** | Análisis inteligente con Azure AI | Embeddings, Document Intelligence, GPT-4 | +| **Transformación** | Manipulan y enriquecen datos | Chunking, mapeo de campos, agregación | +| **Salida** | Almacenan resultados procesados | Blob Storage, Azure AI Search | +| **Enrutamiento** | Lógica condicional y paralelismo | Fan-out/Fan-in, sub-pipelines | +| **Análisis de texto** | Procesamiento de lenguaje natural | Resumen, entidades, sentimiento, PII, idioma | +| **Especializado** | Tareas de dominio específico | Web scraping, grafos de conocimiento | + +### 3.3 Vaults (Bóvedas de documentos) + +Un **vault** es un contenedor lógico que agrupa documentos y los asocia a un pipeline específico. Cuando el vault está habilitado, los workers automáticamente descubren nuevos documentos y los procesan según el pipeline asociado. + +``` +Vault "Facturas 2024" + ├── Pipeline asociado: "procesar-facturas" + ├── Tags: [facturas, contabilidad] + ├── Documentos descubiertos: 1,247 + └── Estado: Habilitado ✓ +``` + +### 3.4 Modelo de Contenido + +Todo el contenido que fluye por un pipeline utiliza un **modelo estandarizado** (`Content`) que acumula datos a medida que pasa por cada ejecutor: + +``` +Content + ├── id: identificador único + ├── source: origen del documento (ej. blob://container/archivo.pdf) + ├── content: texto extraído + ├── metadata: información adicional del documento + ├── chunks: fragmentos de texto + ├── embeddings: vectores numéricos para búsqueda semántica + └── analysis: resultados de análisis IA +``` + +--- + +## 4. Componentes principales + +ContentFlow se compone de **5 componentes principales** que trabajan de forma coordinada: + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ContentFlow │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ │ +│ │ Web │──▶│ API │──▶│ Workers │──▶│ Librería │ │ +│ │ (React) │ │ (FastAPI)│ │ (Python) │ │ (Python) │ │ +│ └──────────┘ └────┬─────┘ └─────┬─────┘ └───────────┘ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────────────────────┐ │ +│ │ Infraestructura Azure │ │ +│ │ (Cosmos DB, Blob, Queue, │ │ +│ │ AI Services, Container │ │ +│ │ Apps, App Configuration) │ │ +│ └──────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4.1 Interfaz Web (contentflow-web) + +La **interfaz web** es una aplicación React moderna que permite a los usuarios interactuar visualmente con ContentFlow: + +- **Constructor visual de pipelines**: Editor drag-and-drop basado en ReactFlow para diseñar pipelines gráficamente +- **Editor YAML**: Editor de código Monaco con resaltado de sintaxis para definir pipelines +- **Gestión de Vaults**: Crear, editar y monitorear bóvedas de documentos +- **Galería de plantillas**: Pipelines pre-construidos listos para usar +- **Panel de ejecuciones**: Ver el estado y resultados del procesamiento en tiempo real + +**Tecnologías**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn/ui, ReactFlow, Monaco Editor + +### 4.2 API REST (contentflow-api) + +El **servicio API** es el punto central de coordinación. Recibe solicitudes de la interfaz web y orquesta las operaciones: + +- **Gestión de pipelines**: Crear, leer, actualizar, eliminar y ejecutar pipelines +- **Gestión de vaults**: Administrar bóvedas de documentos +- **Catálogo de ejecutores**: Explorar los ejecutores disponibles y sus configuraciones +- **Verificación de salud**: Monitorear el estado de todos los servicios dependientes + +**Tecnologías**: FastAPI, Python 3.12+, Uvicorn, Pydantic, Azure SDK + +**Puerto**: 8090 + +### 4.3 Workers de procesamiento (contentflow-worker) + +Los **workers** son el motor de ejecución distribuida. Procesan el contenido de forma asíncrona utilizando múltiples procesos: + +- **Workers de entrada (Source Workers)**: Descubren contenido nuevo en los orígenes de datos configurados +- **Workers de procesamiento (Processing Workers)**: Ejecutan los pipelines sobre cada elemento de contenido + +**Funcionamiento simplificado**: +``` +Worker de Entrada Worker de Procesamiento + │ │ + ├── Lee vaults habilitados ├── Consulta la cola + ├── Ejecuta ejecutores de entrada ├── Recibe tarea + ├── Descubre documentos nuevos ├── Lee el pipeline + └── Crea tareas en la cola └── Ejecuta pipeline + sobre el contenido +``` + +**Tecnologías**: Python 3.12+, multiprocessing, Azure Storage Queue + +**Puerto**: 8099 (API de salud) + +### 4.4 Librería de procesamiento (contentflow-lib) + +La **librería** es el corazón técnico de ContentFlow. Contiene: + +- **Motor de pipelines**: Parsea YAML y ejecuta grafos dirigidos acíclicos (DAG) +- **+35 ejecutores**: Implementaciones listas para usar +- **Modelo de contenido**: Estructura de datos estandarizada +- **Conectores Azure**: Clientes para Blob, Cosmos DB, AI Search, OpenAI, Document Intelligence + +Esta librería se instala como dependencia en el API y el Worker. + +### 4.5 Infraestructura (infra) + +Plantillas **Bicep** (Infrastructure as Code) para aprovisionar todos los recursos de Azure necesarios. Se despliega con un solo comando `azd up`. + +--- + +## 5. ¿Cómo funciona el flujo completo? + +### 5.1 Paso a paso: De un documento a datos inteligentes + +``` + ① USUARIO ② API ③ COLA + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ Crea o edita│ ─────────▶ │ Almacena en │ ─────────▶ │ Encola │ + │ un pipeline │ REST API │ Cosmos DB y │ Storage │ tarea de │ + │ y ejecuta │ │ crea tarea │ Queue │ descubrir │ + └─────────────┘ └─────────────┘ └──────┬──────┘ + │ + ⑥ RESULTADOS ⑤ PROCESAMIENTO ④ DESCUBRIMIENTO + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ Datos en │ ◀───────── │ Ejecuta cada│ ◀───────── │ Worker lee │ + │ Blob Storage│ Guardado │ ejecutor del│ Cola de │ el origen y │ + │ y Cosmos DB │ │ pipeline │ tareas │ lista docs │ + └─────────────┘ └─────────────┘ └─────────────┘ +``` + +**Flujo detallado**: + +1. **El usuario** crea un pipeline (visualmente o en YAML) y lo ejecuta desde la interfaz web +2. **La API** recibe la solicitud, guarda la configuración en Cosmos DB y crea un registro de ejecución +3. **La API** envía una tarea de tipo `InputSource` a la cola de Azure Storage +4. **El Worker de Entrada** lee la tarea, ejecuta los ejecutores de entrada del pipeline y descubre los documentos disponibles +5. **Por cada documento** descubierto, el Worker de Entrada crea una tarea `ContentProcessing` en la cola +6. **Los Workers de Procesamiento** toman las tareas de la cola y ejecutan el pipeline completo (extracción → transformación → análisis → salida) sobre cada documento +7. **Los resultados** se almacenan en Blob Storage y/o Cosmos DB +8. **El usuario** puede monitorear el progreso y ver los resultados desde la interfaz web + +### 5.2 Diagrama de flujo general + +``` +┌─────────────┐ HTTP/REST ┌─────────────────┐ +│ │ ──────────────────▶ │ │ +│ Frontend │ │ API Service │ +│ (React) │ ◀────────────────── │ (FastAPI) │ +│ │ Respuestas │ │ +└─────────────┘ └───────┬─────────┘ + │ + ┌───────────────┼──────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌────────────┐ ┌────────────┐ ┌──────────────┐ + │ Cosmos DB │ │ Blob │ │ Storage │ + │ (Metadatos │ │ Storage │ │ Queue │ + │ y estado) │ │(Documentos)│ │ (Tareas) │ + └──────┬─────┘ └─────┬──────┘ └───────┬──────┘ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────────────────────────────────────┐ + │ Worker Service │ + │ ┌─────────────────┐ ┌──────────────────────┐ │ + │ │ Source Workers │ │ Processing Workers │ │ + │ │ (Descubrimiento) │ │ (Ejecución pipeline) │ │ + │ └────────┬────────┘ └──────────┬───────────┘ │ + │ │ │ │ + │ ▼ ▼ │ + │ ┌────────────────────────────────────────────┐ │ + │ │ contentflow-lib │ │ + │ │ (Motor de pipelines + 35+ Ejecutores) │ │ + │ └────────────────────┬───────────────────────┘ │ + └───────────────────────┼─────────────────────────┘ + │ + ▼ + ┌──────────────────────────┐ + │ Azure AI Services │ + │ ┌──────┐ ┌──────────┐ │ + │ │GPT-4 │ │ Document │ │ + │ │OpenAI│ │ Intelli- │ │ + │ │ │ │ gence │ │ + │ └──────┘ └──────────┘ │ + └──────────────────────────┘ +``` + +--- + +## 6. Servicios de Azure utilizados + +| Servicio de Azure | Propósito en ContentFlow | SKU / Nivel | +|-------------------|--------------------------|-------------| +| **Azure Container Apps** | Hospeda los 3 servicios (API, Worker, Web) | Consumption (serverless) | +| **Azure Cosmos DB** | Base de datos para metadatos, pipelines, ejecuciones | Serverless | +| **Azure Blob Storage** | Almacena documentos y contenido procesado | Standard_LRS, Hot | +| **Azure Storage Queue** | Cola de mensajes para distribución de tareas | Incluido en Storage | +| **Azure App Configuration** | Configuración centralizada de todos los servicios | Standard | +| **Azure Container Registry** | Registro de imágenes Docker | Standard (Premium con endpoints privados) | +| **Azure Log Analytics** | Workspace de logs y telemetría | PerGB2018 | +| **Azure Application Insights** | Monitoreo y diagnósticos | Conectado a Log Analytics | +| **Azure AI Foundry** | Plataforma de modelos de IA | S0 | +| **GPT-4.1 / GPT-4.1-mini** | Modelos de lenguaje para análisis inteligente | GlobalStandard, Capacidad: 100 | +| **Azure Document Intelligence** | Extracción inteligente de documentos (OCR) | Según integración | +| **Managed Identity** | Autenticación sin contraseñas entre servicios | User-Assigned | + +--- + +## 7. Modos de despliegue + +ContentFlow soporta dos modos de despliegue para adaptarse a diferentes necesidades: + +### 7.1 Modo Básico + +Ideal para **desarrollo, pruebas y demos**: + +- Endpoints públicos accesibles desde internet +- Red virtual creada automáticamente +- Todos los recursos se crean nuevos +- Sin endpoints privados +- Despliegue rápido y sencillo + +```bash +DEPLOYMENT_MODE=basic +azd up +``` + +### 7.2 Modo AILZ (AI Landing Zone) + +Diseñado para **entornos de producción empresarial**: + +- Se integra con una Azure AI Landing Zone existente +- Endpoints privados para todos los servicios +- Red virtual compartida con subnets dedicadas +- Zonas DNS privadas para resolución interna +- Cumplimiento con estándares de seguridad empresarial + +```bash +DEPLOYMENT_MODE=ailz-integrated +azd up +``` + +``` +┌─────────────────────────────────────────────────────────┐ +│ AI Landing Zone VNet │ +│ │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ aca-env-subnet │ │ pe-subnet │ │ +│ │ ┌────────────┐ │ │ ┌──────┐ ┌──────────┐ │ │ +│ │ │ Container │ │ │ │Cosmos│ │ Storage │ │ │ +│ │ │ Apps (API, │ │ │ │ DB │ │ Account │ │ │ +│ │ │ Worker,Web)│ │ │ │(PE) │ │ (PE) │ │ │ +│ │ └────────────┘ │ │ └──────┘ └──────────┘ │ │ +│ └──────────────────┘ └──────────────────────────┘ │ +│ │ +│ PE = Private Endpoint │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 8. Ejemplo práctico: Pipeline de documentos PDF + +A continuación, un ejemplo concreto de cómo ContentFlow procesa un conjunto de documentos PDF para crear un índice de búsqueda inteligente: + +**Pipeline: "Indexación de documentos PDF"** + +```yaml +name: indexacion-pdf +steps: + # 1. Descubrir PDFs en Blob Storage + - executor: azure_blob_input_discovery + settings: + blob_container_name: documentos-legales + file_extensions: [".pdf"] + + # 2. Recuperar contenido del blob + - executor: azure_blob_content_retriever + + # 3. Extraer texto con Azure Document Intelligence + - executor: azure_document_intelligence_extractor + settings: + model_id: prebuilt-layout + + # 4. Dividir en fragmentos manejables + - executor: recursive_text_chunker + settings: + chunk_size: 1000 + chunk_overlap: 200 + + # 5. Generar vectores para búsqueda semántica + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + # 6. Indexar en Azure AI Search + - executor: ai_search_index_output + settings: + index_name: documentos-legales-index +``` + +**Resultado**: Cada PDF se convierte en fragmentos indexados con vectores semánticos, listos para búsquedas inteligentes tipo "¿cuáles son las cláusulas de rescisión?". + +--- + +## 9. Resumen ejecutivo + +| Aspecto | Detalle | +|---------|---------| +| **Tipo de solución** | Plataforma de procesamiento de contenido empresarial | +| **Arquitectura** | Microservicios, event-driven, cloud-native | +| **Nube** | Microsoft Azure | +| **Servicios** | 3 contenedores: API, Worker, Web | +| **Motor de IA** | GPT-4.1, Document Intelligence, Embeddings | +| **Base de datos** | Cosmos DB (Serverless) | +| **Almacenamiento** | Azure Blob Storage | +| **Mensajería** | Azure Storage Queue | +| **Ejecutores disponibles** | 35+ ejecutores pre-construidos | +| **Formatos soportados** | PDF, Word, Excel, PowerPoint, CSV, HTML, web | +| **Seguridad** | Managed Identity, RBAC, sin contraseñas | +| **Despliegue** | `azd up` — un solo comando | +| **Modos** | Básico (desarrollo) o AILZ (producción empresarial) | + +--- + +> **ContentFlow** simplifica el camino desde documentos crudos hasta datos inteligentes, combinando la potencia de Azure AI con una arquitectura escalable y una experiencia de usuario intuitiva. diff --git a/Analysis/02-architecture-detailed.md b/Analysis/02-architecture-detailed.md new file mode 100644 index 0000000..a3f9f6e --- /dev/null +++ b/Analysis/02-architecture-detailed.md @@ -0,0 +1,1920 @@ +# ContentFlow — Arquitectura Detallada de Componentes + +> **Documentación técnica completa**: componentes, interconexiones, flujos de datos, SKUs, configuración y diagramas de la plataforma ContentFlow. + +--- + +## Índice + +- [1. Visión general de la arquitectura](#1-visión-general-de-la-arquitectura) + - [1.1 Diagrama de arquitectura completa](#11-diagrama-de-arquitectura-completa) + - [1.2 Principios arquitectónicos](#12-principios-arquitectónicos) +- [2. Componente: Interfaz Web (contentflow-web)](#2-componente-interfaz-web-contentflow-web) + - [2.1 Stack tecnológico](#21-stack-tecnológico) + - [2.2 Estructura de la aplicación](#22-estructura-de-la-aplicación) + - [2.3 Rutas y navegación](#23-rutas-y-navegación) + - [2.4 Componentes principales de la UI](#24-componentes-principales-de-la-ui) + - [2.5 Gestión de estado y datos](#25-gestión-de-estado-y-datos) + - [2.6 Diagrama de componentes Web](#26-diagrama-de-componentes-web) +- [3. Componente: API REST (contentflow-api)](#3-componente-api-rest-contentflow-api) + - [3.1 Stack tecnológico](#31-stack-tecnológico) + - [3.2 Endpoints del API](#32-endpoints-del-api) + - [3.3 Capa de servicios](#33-capa-de-servicios) + - [3.4 Capa de base de datos](#34-capa-de-base-de-datos) + - [3.5 Modelos de datos](#35-modelos-de-datos) + - [3.6 Inyección de dependencias](#36-inyección-de-dependencias) + - [3.7 Configuración y arranque](#37-configuración-y-arranque) + - [3.8 Diagrama de capas del API](#38-diagrama-de-capas-del-api) +- [4. Componente: Worker Service (contentflow-worker)](#4-componente-worker-service-contentflow-worker) + - [4.1 Stack tecnológico](#41-stack-tecnológico) + - [4.2 Arquitectura multi-proceso](#42-arquitectura-multi-proceso) + - [4.3 Worker de entrada (Input Source Worker)](#43-worker-de-entrada-input-source-worker) + - [4.4 Worker de procesamiento (Content Processing Worker)](#44-worker-de-procesamiento-content-processing-worker) + - [4.5 Cliente de cola](#45-cliente-de-cola) + - [4.6 Ciclo de vida de una tarea](#46-ciclo-de-vida-de-una-tarea) + - [4.7 Configuración del Worker](#47-configuración-del-worker) + - [4.8 Diagrama del motor de workers](#48-diagrama-del-motor-de-workers) +- [5. Componente: Librería Core (contentflow-lib)](#5-componente-librería-core-contentflow-lib) + - [5.1 Motor de pipelines](#51-motor-de-pipelines) + - [5.2 Jerarquía de clases de ejecutores](#52-jerarquía-de-clases-de-ejecutores) + - [5.3 Catálogo completo de ejecutores](#53-catálogo-completo-de-ejecutores) + - [5.4 Modelo de contenido (Content)](#54-modelo-de-contenido-content) + - [5.5 Conectores de Azure](#55-conectores-de-azure) + - [5.6 Diagrama del motor de pipelines](#56-diagrama-del-motor-de-pipelines) +- [6. Infraestructura de Azure (infra)](#6-infraestructura-de-azure-infra) + - [6.1 Recursos desplegados](#61-recursos-desplegados) + - [6.2 SKUs y niveles de servicio](#62-skus-y-niveles-de-servicio) + - [6.3 Módulos Bicep](#63-módulos-bicep) + - [6.4 Cosmos DB — Estructura de base de datos](#64-cosmos-db--estructura-de-base-de-datos) + - [6.5 Storage Account — Blobs y colas](#65-storage-account--blobs-y-colas) + - [6.6 Container Apps — Configuración de servicios](#66-container-apps--configuración-de-servicios) + - [6.7 AI Foundry — Modelos de IA](#67-ai-foundry--modelos-de-ia) + - [6.8 Diagrama de infraestructura Azure](#68-diagrama-de-infraestructura-azure) +- [7. Interconexión entre componentes](#7-interconexión-entre-componentes) + - [7.1 Flujo de datos completo](#71-flujo-de-datos-completo) + - [7.2 Flujo de mensajes por cola](#72-flujo-de-mensajes-por-cola) + - [7.3 Flujo de autenticación](#73-flujo-de-autenticación) + - [7.4 Matriz de comunicación entre servicios](#74-matriz-de-comunicación-entre-servicios) + - [7.5 Diagrama de secuencia: Ejecución de pipeline](#75-diagrama-de-secuencia-ejecución-de-pipeline) +- [8. Modos de despliegue](#8-modos-de-despliegue) + - [8.1 Modo básico](#81-modo-básico) + - [8.2 Modo AILZ (AI Landing Zone)](#82-modo-ailz-ai-landing-zone) + - [8.3 Proceso de despliegue con azd](#83-proceso-de-despliegue-con-azd) + - [8.4 Diagrama comparativo de modos](#84-diagrama-comparativo-de-modos) +- [9. Seguridad y autenticación](#9-seguridad-y-autenticación) + - [9.1 Modelo de identidad](#91-modelo-de-identidad) + - [9.2 Roles RBAC asignados](#92-roles-rbac-asignados) + - [9.3 Diagrama de seguridad](#93-diagrama-de-seguridad) +- [10. Configuración centralizada](#10-configuración-centralizada) + - [10.1 Azure App Configuration](#101-azure-app-configuration) + - [10.2 Variables de entorno por servicio](#102-variables-de-entorno-por-servicio) +- [11. Monitoreo y observabilidad](#11-monitoreo-y-observabilidad) +- [12. Resumen de dependencias entre componentes](#12-resumen-de-dependencias-entre-componentes) + +--- + +## 1. Visión general de la arquitectura + +ContentFlow implementa una **arquitectura de microservicios event-driven (basada en eventos)** con los siguientes patrones clave: + +- **Microservicios**: 3 servicios independientes (API, Worker, Web) desplegados como contenedores +- **Event-Driven**: Comunicación asíncrona mediante colas de mensajes +- **Cloud-Native**: Diseñado para Azure Container Apps con escalado automático +- **Declarativo**: Pipelines definidos en YAML, infraestructura definida en Bicep + +### 1.1 Diagrama de arquitectura completa + +``` +┌──────────────────────────────────────────────────────────────────────────────────┐ +│ AZURE CONTAINER APPS ENVIRONMENT │ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ contentflow-web │ │ contentflow-api │ │contentflow-worker│ │ +│ │ │ │ │ │ │ │ +│ │ React 18 │ │ FastAPI │ │ Multi-process │ │ +│ │ TypeScript │───▶│ Python 3.12+ │ │ Python 3.12+ │ │ +│ │ Vite │ │ Puerto: 8090 │ │ Puerto: 8099 │ │ +│ │ ReactFlow │ │ │ │ │ │ +│ │ Monaco Editor │ │ ┌────────────┐ │ │ ┌──────────────┐ │ │ +│ │ │ │ │ Routers │ │ │ │Source Workers│ │ │ +│ │ │ │ │ Services │ │ │ │Process Worker│ │ │ +│ │ │ │ │ Models │ │ │ │Queue Client │ │ │ +│ │ │ │ └────────────┘ │ │ └──────────────┘ │ │ +│ │ CPU: 1 | 2Gi │ │ CPU: 1 | 2Gi │ │ CPU: 1 | 2Gi │ │ +│ │ Réplicas: 1-2 │ │ Réplicas: 1-2 │ │ Réplicas: 1-2 │ │ +│ └──────────────────┘ └───────┬──────────┘ └────────┬─────────┘ │ +│ │ │ │ +└──────────────────────────────────┼─────────────────────────┼─────────────────────┘ + │ │ + ┌───────────────────────┼─────────────────────────┼──────────────┐ + │ ▼ ▼ │ + │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │ + │ │ Cosmos DB │ │ Blob │ │ Storage Queue │ │ + │ │ (Serverless)│ │ Storage │ │ (contentflow- │ │ + │ │ │ │ (Standard │ │ execution-requests) │ │ + │ │ 7 containers│ │ LRS, Hot) │ │ │ │ + │ └─────────────┘ └─────────────┘ └──────────────────────┘ │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │ + │ │ App Config │ │ App Insights │ │ AI Foundry (S0) │ │ + │ │ (Standard) │ │ + Log Analyt.│ │ GPT-4.1 │ │ + │ │ │ │ (PerGB2018) │ │ GPT-4.1-mini │ │ + │ └──────────────┘ └──────────────┘ └────────────────────┘ │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ │ + │ │ Container │ │ Managed │ │ + │ │ Registry │ │ Identity │ │ + │ │ (Standard) │ │ (User Assign)│ │ + │ └──────────────┘ └──────────────┘ │ + │ │ + │ SERVICIOS DE AZURE │ + └────────────────────────────────────────────────────────────────┘ +``` + +### 1.2 Principios arquitectónicos + +| Principio | Implementación | +|-----------|---------------| +| **Desacoplamiento** | Los servicios se comunican a través de colas y base de datos compartida, no llamadas directas | +| **Escalabilidad horizontal** | Workers pueden escalarse independientemente (1-2 réplicas con autoescalado) | +| **Procesamiento asíncrono** | Las tareas se encolan y procesan en segundo plano | +| **Resiliencia** | Reintentos configurables (3 por defecto), timeouts, y manejo de errores por ejecutor | +| **Seguridad Zero-Trust** | Managed Identity, sin contraseñas compartidas, RBAC para cada recurso | +| **Configuración centralizada** | Azure App Configuration como fuente única de verdad | +| **Observabilidad** | Application Insights + Log Analytics para trazabilidad distribuida | + +--- + +## 2. Componente: Interfaz Web (contentflow-web) + +### 2.1 Stack tecnológico + +| Tecnología | Versión | Rol | +|------------|---------|-----| +| React | 18.3.1 | Framework UI | +| TypeScript | 5.8.3 | Lenguaje tipado | +| Vite | 7.1.5 | Empaquetador y servidor de desarrollo | +| Tailwind CSS | 3.4.17 | Framework de estilos utilitarios | +| ReactFlow | 11.11.4 | Visualización de grafos (pipeline builder) | +| Monaco Editor | 4.7.0 | Editor de código YAML | +| TanStack Query | 5.83.0 | Gestión de estado del servidor | +| React Hook Form | 7.61.1 | Formularios con validación | +| Zod | 3.25.76 | Esquemas de validación TypeScript | +| Shadcn/ui + Radix UI | Múltiples | Componentes UI accesibles | +| Recharts | 2.15.4 | Gráficas y visualizaciones | +| Lucide React | 0.462.0 | Iconografía | +| js-yaml | 4.1.1 | Parsing YAML | +| date-fns | 3.6.0 | Utilidades de fecha | + +### 2.2 Estructura de la aplicación + +``` +contentflow-web/src/ +├── main.tsx # Punto de entrada de React +├── App.tsx # Componente raíz con providers y rutas +├── index.css # Estilos globales (Tailwind) +├── components/ +│ ├── ui/ # Componentes base Shadcn/ui +│ │ ├── button.tsx # Botones +│ │ ├── dialog.tsx # Diálogos modales +│ │ ├── input.tsx # Campos de entrada +│ │ ├── select.tsx # Selectores +│ │ ├── toast.tsx # Notificaciones +│ │ ├── tabs.tsx # Pestañas +│ │ ├── card.tsx # Tarjetas +│ │ └── ... # +20 componentes más +│ ├── pipeline/ # Componentes del constructor de pipelines +│ ├── vaults/ # Componentes de gestión de vaults +│ ├── dashboard/ # Componentes del panel principal +│ ├── knowledge/ # Componentes del grafo de conocimiento +│ ├── templates/ # Componentes de plantillas +│ ├── Hero.tsx # Sección de bienvenida +│ ├── PipelineBuilder.tsx # Constructor visual de pipelines +│ ├── KnowledgeGraph.tsx # Visualizador de grafos +│ ├── Footer.tsx # Pie de página +│ └── NavLink.tsx # Enlace de navegación +├── pages/ +│ ├── Index.tsx # Página principal (home, pipeline, vaults) +│ ├── Templates.tsx # Galería de plantillas +│ ├── Vaults.tsx # Gestión de vaults (ruta directa) +│ └── NotFound.tsx # Página 404 +├── hooks/ # Custom hooks de React +├── types/ # Definiciones de tipos TypeScript +├── lib/ # Utilidades y configuración (cn, utils) +└── data/ # Datos mock y constantes +``` + +### 2.3 Rutas y navegación + +| Ruta | Componente | Vista | Descripción | +|------|-----------|-------|-------------| +| `/` | Index | home | Página principal con hero y navegación | +| `/?view=pipeline` | Index | pipeline | Constructor visual de pipelines | +| `/?view=graph` | Index | graph | Visualizador de grafos de conocimiento | +| `/?view=vaults` | Index | vaults | Gestión de bóvedas de documentos | +| `/templates` | Templates | — | Galería de plantillas pre-construidas | +| `/*` | NotFound | — | Página 404 | + +### 2.4 Componentes principales de la UI + +``` +┌────────────────────────────────────────────────────────────────┐ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Barra de Navegación │ │ +│ │ [Logo] ContentFlow | Home | Pipeline | Vaults | │ │ +│ │ Templates │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ CONTENIDO PRINCIPAL (según vista activa) │ │ +│ │ │ │ +│ │ ┌─ Home ──────────────────────────────────────────┐ │ │ +│ │ │ Hero: Bienvenida y métricas del sistema │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─ Pipeline Builder ──────────────────────────────┐ │ │ +│ │ │ ┌───────────┐ ┌───────────────────────────┐ │ │ │ +│ │ │ │ Catálogo │ │ Lienzo ReactFlow │ │ │ │ +│ │ │ │ de │ │ (Nodos + Aristas) │ │ │ │ +│ │ │ │ ejecutores│ │ │ │ │ │ +│ │ │ │ │ │ [Nodo A] ──▶ [Nodo B] │ │ │ │ +│ │ │ │ • Input │ │ │ │ │ │ │ +│ │ │ │ • Extract │ │ ▼ │ │ │ │ +│ │ │ │ • Process │ │ [Nodo C] ──▶ [Nodo D] │ │ │ │ +│ │ │ │ • Output │ │ │ │ │ │ +│ │ │ └───────────┘ └───────────────────────────┘ │ │ │ +│ │ │ ┌─────────────────────────────────────────────┐│ │ │ +│ │ │ │ Editor YAML (Monaco) ││ │ │ +│ │ │ └─────────────────────────────────────────────┘│ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─ Vaults ────────────────────────────────────────┐ │ │ +│ │ │ Lista de bóvedas │ Detalle │ Ejecuciones │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Footer │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────┘ +``` + +### 2.5 Gestión de estado y datos + +| Librería | Responsabilidad | Patrón | +|----------|----------------|--------| +| **TanStack Query** | Estado del servidor (datos de la API) | Cache con invalidación automática | +| **React Hook Form** | Estado de formularios | Formularios controlados con validación | +| **Zod** | Validación de esquemas | Validación en tiempo de ejecución | +| **URL Query Params** | Navegación y vista activa | Estado en la URL (ej. `?view=pipeline`) | +| **React Context** | Estado global de la app (si aplica) | Provider pattern | + +### 2.6 Diagrama de componentes Web + +``` + App.tsx + │ + ┌───────────┼───────────────┐ + │ │ │ + QueryClient BrowserRouter Providers + Provider │ (Tooltip, + │ │ Toast, + │ ┌─────┴──────┐ Sonner) + │ │ │ + │ Route / Route /templates + │ │ │ + │ Index Templates + │ │ + │ ├── Hero + │ ├── PipelineBuilder ──▶ ReactFlow + Monaco + │ ├── KnowledgeGraph ──▶ Visualización de grafos + │ └── Vaults ──▶ CRUD de bóvedas + │ + └── Comunicación con API vía fetch/httpx + (GET/POST/PUT/DELETE → /api/*) +``` + +--- + +## 3. Componente: API REST (contentflow-api) + +### 3.1 Stack tecnológico + +| Tecnología | Versión | Rol | +|------------|---------|-----| +| Python | 3.12+ | Runtime | +| FastAPI | 0.128.0 | Framework web asíncrono | +| Uvicorn | 0.40.0 | Servidor ASGI | +| Pydantic | 2.12+ | Validación de datos y modelos | +| pydantic-settings | 2.12.0 | Gestión de configuración | +| azure-identity | 1.25.1 | Autenticación con Managed Identity | +| azure-cosmos | 4.14.3 | Cliente Cosmos DB NoSQL | +| azure-storage-blob | 12.27.1 | Cliente Blob Storage | +| azure-storage-queue | 12.14.1 | Cliente Storage Queue | +| azure-appconfiguration-provider | 2.3.1 | Configuración centralizada | +| httpx | 0.28.1 | Cliente HTTP asíncrono | +| aiohttp | 3.13.3+ | Cliente HTTP asíncrono alternativo | +| PyYAML | 6.0.3+ | Parsing YAML | +| python-dotenv | 1.2.1+ | Variables de entorno locales | +| contentflow-lib | local | Librería de procesamiento (editable) | + +### 3.2 Endpoints del API + +#### Salud del sistema + +| Método | Ruta | Descripción | Respuesta | +|--------|------|-------------|-----------| +| `GET` | `/api/health/` | Verificación completa de todos los servicios | `SystemHealth` | +| `GET` | `/api/health/{service}` | Verificar un servicio específico | `ServiceHealth` | + +Servicios verificados: `cosmos_db`, `storage_queue`, `app_config` + +#### Gestión de Pipelines + +| Método | Ruta | Descripción | Request Body | Respuesta | +|--------|------|-------------|-------------|-----------| +| `GET` | `/api/pipelines/` | Listar todos los pipelines | — | `List[Pipeline]` | +| `GET` | `/api/pipelines/{id_or_name}` | Obtener por ID o nombre | — | `Pipeline` | +| `POST` | `/api/pipelines/` | Crear o actualizar pipeline | `SavePipelineRequest` | `Pipeline` | +| `DELETE` | `/api/pipelines/{id}` | Eliminar pipeline | — | `bool` | +| `POST` | `/api/pipelines/{id}/execute` | Ejecutar pipeline | `ExecutePipelineRequest` | `ExecutePipelineResponse` | + +#### Gestión de Vaults + +| Método | Ruta | Descripción | Parámetros | Respuesta | +|--------|------|-------------|-----------|-----------| +| `GET` | `/api/vaults/` | Listar vaults | `search`, `tags` | `List[Vault]` | +| `GET` | `/api/vaults/{id}` | Obtener vault por ID | — | `Vault` | +| `POST` | `/api/vaults/` | Crear vault | Body: `VaultCreateRequest` | `Vault` | +| `PUT` | `/api/vaults/{id}` | Actualizar vault | Body: `VaultUpdateRequest` | `Vault` | +| `DELETE` | `/api/vaults/{id}` | Eliminar vault | — | `bool` | + +#### Catálogo de Ejecutores + +| Método | Ruta | Descripción | Respuesta | +|--------|------|-------------|-----------| +| `GET` | `/api/executors/` | Listar todos los ejecutores | `List[ExecutorCatalogDefinition]` | +| `GET` | `/api/executors/{id}` | Obtener ejecutor por ID | `ExecutorCatalogDefinition` | + +#### Raíz + +| Método | Ruta | Descripción | +|--------|------|-------------| +| `GET` | `/` | Mensaje de bienvenida y versión del API | + +### 3.3 Capa de servicios + +Cada servicio implementa lógica de negocio específica heredando de `BaseService`: + +``` +BaseService (CRUD genérico sobre Cosmos DB) + │ + ├── PipelineService + │ ├── get_pipelines() → List[Pipeline] + │ ├── get_pipeline_by_id(id) → Pipeline + │ ├── get_pipeline_by_name(name) → Pipeline + │ ├── create_pipeline(data) → Pipeline + │ ├── create_or_save_pipeline(data) → Pipeline + │ ├── update_by_id(id, data) → Pipeline + │ └── delete_pipeline_by_id(id) → bool + │ + ├── VaultService + │ ├── create_vault(request, pipeline_name) → Vault + │ ├── get_vault(id) → Vault + │ ├── list_vaults(search, tags) → List[Vault] + │ ├── update_vault(id, request) → Vault + │ └── delete_vault(id) → bool + │ + ├── VaultExecutionService + │ ├── get_vault_executions(vault_id, start, end) → List[VaultExecution] + │ ├── get_vault_crawl_checkpoints(vault_id) → List[Checkpoint] + │ ├── delete_vault_executions(vault_id) + │ └── delete_vault_crawl_checkpoints(vault_id) + │ + ├── PipelineExecutionService + │ └── Seguimiento de ejecuciones de pipeline + │ + ├── ExecutorCatalogService + │ └── Gestión del catálogo de ejecutores + │ + └── HealthService + ├── check_all_services() → SystemHealth + └── check_service_health(name) → ServiceHealth +``` + +### 3.4 Capa de base de datos + +**CosmosDBClient** — Wrapper asíncrono sobre el SDK oficial de Azure Cosmos DB: + +``` +CosmosDBClient + │ + ├── connect() # Inicializar conexión con credenciales + ├── close() # Cerrar conexión + │ + ├── create(container, item) # Crear documento + ├── get_by_id(container, id) # Obtener por ID + ├── list_all(container, query) # Listar con consulta SQL + ├── query(container, query, params) # Consulta parametrizada + ├── update(container, item) # Upsert (crear o actualizar) + ├── delete(container, id) # Eliminar + └── batch_upsert(container, items) # Actualización masiva +``` + +**Características**: +- Autenticación via `ChainedTokenCredential` (Managed Identity) +- Creación automática de la base de datos si no existe +- Consultas cross-partition habilitadas +- Pool de conexiones compartido + +### 3.5 Modelos de datos + +``` +CosmosBaseModel (Base) + │ + ├── id: str (UUID v4 auto-generado) + ├── created_at: datetime + └── modified_at: datetime + +Pipeline (Definición de pipeline) + ├── name: str + ├── description: str + ├── yaml: str (definición YAML del pipeline) + ├── nodes: List[Any] (nodos del grafo visual) + ├── edges: List[Any] (aristas del grafo visual) + ├── tags: List[str] + ├── enabled: bool + ├── retry_delay: int (segundos, default: 5) + ├── timeout: int (segundos, default: 600) + ├── retries: int (default: 3) + └── version: str + +PipelineExecution (Ejecución de pipeline) + ├── pipeline_id: str + ├── pipeline_name: str + ├── status: ExecutionStatus (pending|running|completed|failed|cancelled) + ├── inputs: Dict + ├── configuration: Dict + ├── outputs: Dict + ├── started_at: str (ISO) + ├── completed_at: str (ISO) + ├── duration_seconds: float + ├── error: str + ├── executor_outputs: Dict[str, ExecutorOutput] + └── events: List[PipelineExecutionEvent] + +Vault (Bóveda de documentos) + ├── name: str (1-100 caracteres) + ├── description: str (max 500) + ├── pipeline_id: str + ├── pipeline_name: str + ├── tags: List[str] + ├── save_execution_output: bool + ├── enabled: bool + └── created_by: str + +VaultExecution (Ejecución de vault) + ├── pipeline_id / vault_id: str + ├── status: pending|running|completed|failed + ├── task_id: str + ├── source_worker_id / processing_worker_id: str + ├── executor_outputs: Dict + ├── events: List[Dict] + ├── content: List[Dict] + └── number_of_items: int + +VaultCrawlCheckpoint (Punto de control de rastreo) + ├── pipeline_id / vault_id / executor_id: str + ├── checkpoint_timestamp: str + └── worker_id: str +``` + +### 3.6 Inyección de dependencias + +FastAPI utiliza inyección de dependencias para proporcionar clientes compartidos a los routers: + +``` +get_config_provider() ──▶ ConfigurationProvider (Azure App Config) + │ │ + ▼ ▼ +get_cosmos_client() ──▶ CosmosDBClient (singleton) + │ + ├──▶ get_pipeline_service() ──▶ PipelineService + ├──▶ get_vault_service() ──▶ VaultService + ├──▶ get_vault_execution_service() ──▶ VaultExecutionService + ├──▶ get_executor_catalog_service() ──▶ ExecutorCatalogService + ├──▶ get_pipeline_execution_service() ──▶ PipelineExecutionService + └──▶ get_health_service() ──▶ HealthService + +Todas las dependencias usan un TTL de caché de 10 minutos. +``` + +### 3.7 Configuración y arranque + +**Fuentes de configuración** (en orden de prioridad): + +1. **Variables de entorno** (ej. `.env` para desarrollo local) +2. **Azure App Configuration** (producción) +3. **Valores por defecto** (hardcoded en `settings.py`) + +**Secuencia de arranque del API**: + +``` +main.py + │ + ├── 1. Crear aplicación FastAPI + ├── 2. Configurar CORS (allow_origins: ["*"]) + ├── 3. Registrar routers (/api/health, /api/pipelines, /api/vaults, /api/executors) + │ + └── Lifespan (async context manager): + ├── STARTUP: + │ ├── initialize_cosmos() → Conectar a Cosmos DB + │ ├── initialize_blob_storage() → Inicializar Blob Storage + │ └── initialize_executor_catalog() → Cargar catálogo de ejecutores + │ + └── SHUTDOWN: + └── Limpieza de recursos +``` + +**Configuración del servidor**: + +| Parámetro | Valor | Descripción | +|-----------|-------|-------------| +| Host | `0.0.0.0` | Escuchar en todas las interfaces | +| Puerto | `8090` | Puerto HTTP | +| Workers | `1` | Un proceso Uvicorn | +| Reload | `True` (en DEBUG) | Recarga automática en desarrollo | +| OpenAPI docs | `/docs` | Documentación interactiva Swagger | +| ReDoc | `/redoc` | Documentación alternativa | + +### 3.8 Diagrama de capas del API + +``` +┌─────────────────────────────────────────────────────────┐ +│ CAPA DE PRESENTACIÓN │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ +│ │ /health │ │/pipelines│ │ /vaults │ │/executors │ │ +│ │ Router │ │ Router │ │ Router │ │ Router │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ +│ │ │ │ │ │ +├───────┼────────────┼────────────┼──────────────┼─────────┤ +│ ▼ ▼ ▼ ▼ │ +│ CAPA DE SERVICIOS │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ +│ │ Health │ │ Pipeline │ │ Vault │ │ Catalog │ │ +│ │ Service │ │ Service │ │ Service │ │ Service │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ +│ │ │ │ │ │ +├───────┼────────────┼────────────┼──────────────┼─────────┤ +│ ▼ ▼ ▼ ▼ │ +│ CAPA DE DATOS │ +│ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ CosmosDBClient (Async) │ │ +│ │ ┌─────────────────────────────────────────────┐ │ │ +│ │ │ ChainedTokenCredential (Managed Identity) │ │ │ +│ │ └─────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ +│ │ BlobStorage │ │ StorageQueue │ │ AppConfig │ │ +│ │ Client │ │ Client │ │ Provider │ │ +│ └──────────────┘ └──────────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 4. Componente: Worker Service (contentflow-worker) + +### 4.1 Stack tecnológico + +| Tecnología | Versión | Rol | +|------------|---------|-----| +| Python | 3.12+ | Runtime | +| multiprocessing | stdlib | Paralelismo de procesos | +| FastAPI | 0.128.0 | API de salud (opcional) | +| azure-storage-queue | 12.14.1 | Consumir tareas de la cola | +| azure-cosmos | 4.14.3 | Leer pipelines y actualizar estados | +| azure-storage-blob | 12.27.1 | Acceso a documentos | +| contentflow-lib | local | Motor de pipelines | + +### 4.2 Arquitectura multi-proceso + +El Worker utiliza el módulo `multiprocessing` de Python para ejecutar múltiples procesos independientes: + +``` +┌──────────────────────────────────────────────────────────┐ +│ WORKER ENGINE │ +│ │ +│ ┌────────────────────────────┐ │ +│ │ Proceso Principal │ │ +│ │ (main.py) │ │ +│ │ │ │ +│ │ ├── Signal Handler │ │ +│ │ ├── WorkerEngine.run() │ │ +│ │ └── API Thread (8099) │ │ +│ └─────────────┬──────────────┘ │ +│ │ │ +│ ┌───────────┴───────────────────────────┐ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌────────────────────────┐ ┌────────────────────────┐ │ +│ │ Source Worker(s) │ │ Processing Worker(s) │ │ +│ │ (InputSourceWorker) │ │ (ContentProcessing │ │ +│ │ │ │ Worker) │ │ +│ │ Cantidad: N_SOURCE │ │ Cantidad: N_PROCESS │ │ +│ │ Default: 1 │ │ Default: 0 │ │ +│ │ │ │ │ │ +│ │ Ciclo: │ │ Ciclo: │ │ +│ │ 1. Leer vaults │ │ 1. Poll cola │ │ +│ │ 2. Ejecutar inputs │ │ 2. Recibir tarea │ │ +│ │ 3. Descubrir contenido│ │ 3. Leer pipeline │ │ +│ │ 4. Crear tareas │ │ 4. Ejecutar pipeline │ │ +│ │ 5. Dormir (300s) │ │ 5. Actualizar estado │ │ +│ └────────────────────────┘ └────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────┐ │ +│ │ multiprocessing.Event (stop_event) │ │ +│ │ → Señal compartida para apagado coordinado │ │ +│ └───────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────┘ +``` + +### 4.3 Worker de entrada (Input Source Worker) + +**Responsabilidad**: Descubrir contenido nuevo en los orígenes de datos configurados. + +**Flujo de ejecución**: + +``` +┌─ Bucle principal ────────────────────────────────┐ +│ │ +│ 1. Consultar Cosmos DB → vaults habilitados │ +│ 2. Para cada vault: │ +│ a. Leer pipeline asociado │ +│ b. Obtener checkpoint más reciente │ +│ c. Ejecutar ejecutor(es) de input │ +│ d. Descubrir documentos nuevos │ +│ e. Por cada documento nuevo: │ +│ - Crear ContentProcessingTask │ +│ - Encolar en Storage Queue │ +│ f. Guardar nuevo checkpoint │ +│ 3. Dormir (DEFAULT_POLLING_INTERVAL_SECONDS) │ +│ 4. Repetir hasta stop_event │ +│ │ +└───────────────────────────────────────────────────┘ +``` + +**Gestión de checkpoints**: El sistema implementa **rastreo incremental** para evitar reprocesar documentos: +- En la primera ejecución, no hay checkpoint (se descubren todos los documentos) +- En ejecuciones posteriores, solo se descubren documentos nuevos o modificados desde el último checkpoint +- Los checkpoints se almacenan en el contenedor `vault_crawl_checkpoints` de Cosmos DB + +### 4.4 Worker de procesamiento (Content Processing Worker) + +**Responsabilidad**: Ejecutar pipelines completos sobre elementos de contenido individuales. + +**Flujo de ejecución**: + +``` +┌─ Bucle principal ────────────────────────────────┐ +│ │ +│ 1. Poll Azure Storage Queue │ +│ (max 32 mensajes, visibilidad 300s) │ +│ 2. Para cada mensaje: │ +│ a. Deserializar ContentProcessingTask │ +│ b. Obtener bloqueo distribuido │ +│ c. Leer pipeline de Cosmos DB │ +│ d. Crear PipelineExecutor │ +│ e. Ejecutar pipeline (sin ejecutores input) │ +│ f. Actualizar estado en Cosmos DB │ +│ g. Eliminar mensaje de la cola │ +│ 3. Si no hay mensajes → dormir (5s) │ +│ 4. Repetir hasta stop_event │ +│ │ +└───────────────────────────────────────────────────┘ +``` + +### 4.5 Cliente de cola + +**TaskQueueClient** — Abstracción sobre Azure Storage Queue: + +``` +TaskQueueClient + │ + ├── ensure_queue_exists() + │ → Crea la cola si no existe + │ + ├── send_content_processing_task(task: ContentProcessingTask) + │ → Serializa tarea → JSON → Base64 → Envía a cola + │ + ├── send_input_source_task(task: InputSourceTask) + │ → Serializa tarea → JSON → Base64 → Envía a cola + │ + └── _send_message(message, visibility_timeout) + → Envío interno con manejo de errores +``` + +**Formato del mensaje en cola**: +```json +{ + "task_type": "CONTENT_PROCESSING", + "payload": { + "pipeline_id": "abc-123", + "vault_id": "def-456", + "content": { ... } + } +} +``` + +### 4.6 Ciclo de vida de una tarea + +``` + ┌──────────┐ + │ PENDING │ ← Tarea creada y encolada + └────┬─────┘ + │ + ┌────▼─────┐ + │ RUNNING │ ← Worker toma la tarea + └────┬─────┘ + │ + ┌────────┼────────┐ + │ │ │ + ┌────▼───┐ ┌──▼───┐ ┌─▼──────┐ + │COMPLETED│ │FAILED│ │CANCELLED│ + └────────┘ └──────┘ └────────┘ + │ │ + │ ┌────▼────┐ + │ │ RETRY? │ (max 3 reintentos) + │ └────┬────┘ + │ │ Sí + │ ┌────▼─────┐ + │ │ PENDING │ + │ └──────────┘ + │ + Resultado almacenado + en Cosmos DB + Blob +``` + +### 4.7 Configuración del Worker + +| Parámetro | Default | Descripción | +|-----------|---------|-------------| +| `WORKER_NAME` | `contentflow-worker` | Nombre del worker | +| `NUM_PROCESSING_WORKERS` | `0` | Cantidad de workers de procesamiento | +| `NUM_SOURCE_WORKERS` | `1` | Cantidad de workers de entrada | +| `QUEUE_POLL_INTERVAL_SECONDS` | `5` | Intervalo de consulta a la cola | +| `QUEUE_VISIBILITY_TIMEOUT_SECONDS` | `300` | Timeout de visibilidad (5 min) | +| `QUEUE_MAX_MESSAGES` | `32` | Máximo de mensajes por poll | +| `DEFAULT_POLLING_INTERVAL_SECONDS` | `300` | Intervalo de descubrimiento (5 min) | +| `LOCK_TTL_SECONDS` | `300` | Tiempo de vida del bloqueo distribuido | +| `MAX_TASK_RETRIES` | `3` | Reintentos máximos por tarea | +| `TASK_TIMEOUT_SECONDS` | `600` | Timeout por tarea (10 min) | +| `API_ENABLED` | `true` | Habilitar API de salud | +| `API_PORT` | `8099` | Puerto del API de salud | + +### 4.8 Diagrama del motor de workers + +``` +┌─────────────────────────────────────────────────────────────┐ +│ WORKER ENGINE │ +│ │ +│ Señales del SO ──▶ Signal Handler ──▶ stop_event.set() │ +│ (SIGINT/SIGTERM) │ +│ │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ WorkerEngine.run() │ │ +│ │ ├── start() │ │ +│ │ │ ├── Crear N source workers (mp.Process) │ │ +│ │ │ └── Crear N processing workers (mp.Process) │ │ +│ │ │ │ │ +│ │ └── monitor loop (mientras no stop_event): │ │ +│ │ ├── Verificar procesos activos │ │ +│ │ ├── Reemplazar workers caídos │ │ +│ │ └── sleep(1 segundo) │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ Apagado: │ +│ 1. stop_event.set() │ +│ 2. join(timeout=30s) por cada worker │ +│ 3. terminate() workers que no respondieron │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 5. Componente: Librería Core (contentflow-lib) + +### 5.1 Motor de pipelines + +El motor de pipelines es el cerebro de ContentFlow. Convierte definiciones YAML declarativas en grafos de ejecución: + +``` +YAML Pipeline Definition + │ + ▼ +┌─────────────────┐ +│ PipelineFactory │ ← Parsea YAML y crea instancias de ejecutores +│ │ +│ parse_yaml() │ +│ create_pipeline()│ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ DAG (Grafo │ ← Grafo dirigido acíclico de ejecutores +│ Dirigido │ +│ Acíclico) │ +│ │ +│ Nodos: Ejecutors│ +│ Aristas: Depend.│ +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│PipelineExecutor │ ← Ejecuta el DAG resolviendo dependencias +│ │ +│ execute() │ Características: +│ │ • Ejecución asíncrona (asyncio) +│ │ • Resolución de dependencias +│ │ • Evaluación de condiciones +│ │ • Manejo de errores por ejecutor +│ │ • Eventos de ejecución +│ │ • Estadísticas de rendimiento +└────────┬────────┘ + │ + ▼ +┌─────────────────┐ +│ PipelineResult │ ← Resultado final de la ejecución +│ │ +│ status: Success │ +│ outputs: {...} │ +│ events: [...] │ +│ duration: 45.2s │ +└─────────────────┘ +``` + +### 5.2 Jerarquía de clases de ejecutores + +``` +Executor (Agent Framework) + │ + └── BaseExecutor (ABC) ─── contentflow/executors/base.py + │ + │ Propiedades comunes: + │ ├── id: str + │ ├── settings: Dict + │ ├── enabled: bool + │ ├── condition: str (evaluación condicional) + │ ├── fail_pipeline_on_error: bool + │ └── debug_mode: bool + │ + │ Métodos: + │ ├── get_setting(key, default) + │ ├── resolve_env_vars(value) + │ ├── _evaluate_condition(content) + │ └── process_input(input, ctx) → Content | List[Content] [ABSTRACTO] + │ + ├── InputExecutor (ABC) ─── contentflow/executors/input_executor.py + │ │ + │ │ Propiedades adicionales: + │ │ ├── polling_interval_seconds: int (300) + │ │ ├── max_results: int (1000) + │ │ └── batch_size: int (100) + │ │ + │ │ Métodos: + │ │ ├── crawl(checkpoint) → (List[Content], token) [ABSTRACTO] + │ │ └── crawl_all(checkpoint) → List[Content] + │ │ + │ └── Implementaciones: + │ └── AzureBlobInputDiscoveryExecutor + │ + └── Ejecutores concretos (35+): + ├── Extracción + ├── Procesamiento IA + ├── Transformación + ├── Salida + ├── Enrutamiento + └── Especializados +``` + +### 5.3 Catálogo completo de ejecutores + +#### Entrada y recuperación de contenido + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `azure_blob_input_discovery` | Azure Blob Input Discovery | Descubre archivos en contenedores Blob Storage | +| `azure_blob_content_retriever` | Azure Blob Content Retriever | Recupera el contenido de blobs descubiertos | +| `content_retriever` | Content Retriever | Recuperador genérico de contenido | + +#### Extracción de documentos + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `pdf_extractor` | PDF Extractor | Extrae texto de documentos PDF | +| `word_extractor` | Word Extractor | Extrae texto de documentos Word (.docx) | +| `excel_extractor` | Excel Extractor | Extrae datos de hojas de cálculo Excel | +| `powerpoint_extractor` | PowerPoint Extractor | Extrae texto de presentaciones PowerPoint | +| `csv_extractor` | CSV Extractor | Extrae datos de archivos CSV | +| `azure_document_intelligence_extractor` | Azure Document Intelligence | Extracción inteligente con OCR y modelos pre-entrenados | +| `azure_content_understanding_extractor` | Azure Content Understanding | Análisis avanzado de contenido con AI | + +#### Procesamiento con IA + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `azure_openai_agent` | Azure OpenAI Agent | Procesamiento con modelos GPT (análisis, generación) | +| `azure_openai_embeddings` | Azure OpenAI Embeddings | Genera vectores de embedding para búsqueda semántica | +| `text_summarizer` | Text Summarizer | Genera resúmenes de texto usando IA | +| `entity_extractor` | Entity Extractor | Extrae entidades nombradas (personas, organizaciones, lugares) | +| `sentiment_analyser` | Sentiment Analyser | Análisis de sentimiento (positivo, negativo, neutro) | +| `content_classifier` | Content Classifier | Clasifica contenido en categorías definidas | +| `pii_detector` | PII Detector | Detecta información personal identificable | +| `keyword_extractor` | Keyword Extractor | Extrae palabras clave y frases importantes | +| `language_detector` | Language Detector | Detecta el idioma del contenido | +| `content_translator` | Content Translator | Traduce contenido entre idiomas | + +#### Transformación de datos + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `recursive_text_chunker` | Recursive Text Chunker | Divide texto en fragmentos con solapamiento configurable | +| `table_row_splitter` | Table Row Splitter | Divide tablas en filas individuales para procesamiento | +| `field_mapper` | Field Mapper | Mapea y transforma campos entre el modelo de contenido | +| `field_selector` | Field Selector | Selecciona campos específicos del contenido | +| `fan_in_aggregator` | Fan-In Aggregator | Agrega resultados de procesamiento paralelo | + +#### Salida y almacenamiento + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `azure_blob_output` | Azure Blob Output | Escribe contenido procesado a Blob Storage | +| `ai_search_index_output` | AI Search Index Output | Indexa contenido en Azure AI Search | +| `gptrag_search_index_document_generator` | GPT RAG Index Generator | Genera documentos para índices RAG | + +#### Enrutamiento y orquestación + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `subpipeline` | Sub-Pipeline | Ejecuta un sub-pipeline anidado | +| `for_each_content` | For Each Content | Itera sobre una lista de contenidos | +| `pass_through` | Pass Through | Pasa contenido sin modificar (debug/testing) | + +#### Conjuntos de documentos + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `document_set_initializer` | Document Set Initializer | Inicializa un conjunto de documentos para procesamiento cruzado | +| `document_set_collector` | Document Set Collector | Recopila documentos en un conjunto | +| `cross_document_comparison` | Cross-Document Comparison | Compara contenido entre múltiples documentos | +| `cross_document_field_aggregator` | Cross-Document Field Aggregator | Agrega campos de múltiples documentos | + +#### Especializados + +| ID | Nombre | Descripción | +|----|--------|-------------| +| `web_scraper` | Web Scraper | Extrae contenido de páginas web (Playwright) | +| `cosmos_db_lookup` | Cosmos DB Lookup | Consulta datos en Azure Cosmos DB | + +### 5.4 Modelo de contenido (Content) + +El modelo `Content` es la estructura de datos central que fluye por los pipelines: + +``` +Content +│ +├── Identificación +│ ├── id: str (UUID único) +│ └── source: str (origen: blob://container/path/file.pdf) +│ +├── Datos extraídos +│ ├── content: str (texto completo extraído) +│ ├── content_type: str (MIME type) +│ └── content_hash: str (hash para detección de cambios) +│ +├── Metadatos +│ ├── metadata: Dict[str, Any] +│ │ ├── filename, size, last_modified +│ │ ├── page_count, language +│ │ └── custom fields... +│ │ +│ └── tags: List[str] +│ +├── Fragmentos y embeddings +│ ├── chunks: List[Dict] (fragmentos de texto) +│ └── embeddings: List[float] (vectores numéricos) +│ +├── Resultados de análisis +│ ├── analysis: Dict[str, Any] +│ │ ├── entities, sentiment, keywords +│ │ ├── summary, classification +│ │ └── pii_detected, language +│ │ +│ └── executor_outputs: Dict[str, Any] +│ └── {executor_id: output_data} +│ +└── Registro de ejecución + └── log_entries: List[ExecutorLogEntry] + └── {executor_id, timestamp, status, duration_ms} +``` + +### 5.5 Conectores de Azure + +``` +contentflow/connectors/ +│ +├── Azure Blob Storage Connector +│ ├── Listar blobs en contenedores +│ ├── Descargar contenido de blobs +│ ├── Subir contenido a blobs +│ └── Gestionar metadatos de blobs +│ +├── Azure AI Search Connector +│ ├── Crear/actualizar índices +│ ├── Indexar documentos +│ └── Búsqueda híbrida (texto + vectores) +│ +├── Azure Document Intelligence Connector +│ ├── Analizar documentos (OCR) +│ ├── Modelos pre-entrenados (facturas, recibos, etc.) +│ └── Modelos personalizados +│ +├── Azure OpenAI Connector +│ ├── Completions (GPT-4.1, GPT-4.1-mini) +│ ├── Embeddings (text-embedding-3-small) +│ └── Chat completions +│ +└── Azure Cosmos DB Connector + ├── Consultas SQL NoSQL + ├── CRUD de documentos + └── Bulk operations +``` + +### 5.6 Diagrama del motor de pipelines + +``` +Definición YAML del Pipeline +┌──────────────────────────────────────────────────┐ +│ name: procesar-documentos │ +│ steps: │ +│ - executor: blob_input ─────────┐ │ +│ - executor: pdf_extractor ──────┐ │ │ +│ - executor: chunker ─────────┐ │ │ │ +│ - executor: embeddings ────┐ │ │ │ │ +│ - executor: blob_output ─┐ │ │ │ │ │ +└────────────────────────────┼─┼─┼──┼──┼────────────┘ + │ │ │ │ │ + PipelineFactory.parse() + │ + ▼ + ┌─ DAG de ejecución ────────────┐ + │ │ + │ [blob_input] │ + │ │ │ + │ ▼ │ + │ [pdf_extractor] │ + │ │ │ + │ ▼ │ + │ [chunker] │ + │ │ │ + │ ▼ │ + │ [embeddings] │ + │ │ │ + │ ▼ │ + │ [blob_output] │ + │ │ + └────────────────────────────────┘ + │ + PipelineExecutor.execute() + │ + ▼ + ┌─ Ejecución paso a paso ───────┐ + │ │ + │ Content ──▶ blob_input │ + │ Content ──▶ pdf_extractor │ + │ Content ──▶ chunker │ + │ Content ──▶ embeddings │ + │ Content ──▶ blob_output │ + │ │ + │ Cada paso: │ + │ 1. Evaluar condición │ + │ 2. Si enabled: ejecutar │ + │ 3. Capturar resultado/error │ + │ 4. Emitir evento │ + │ 5. Pasar al siguiente │ + │ │ + └────────────────────────────────┘ + │ + ▼ + PipelineResult + (status, outputs, + events, duration) +``` + +--- + +## 6. Infraestructura de Azure (infra) + +### 6.1 Recursos desplegados + +ContentFlow despliega los siguientes recursos de Azure: + +``` +Resource Group +│ +├── User-Assigned Managed Identity (id-${token}) +│ +├── Log Analytics Workspace (log-${token}) +│ └── Recolección de logs de todos los servicios +│ +├── Application Insights (appi-${token}) +│ └── Monitoreo, métricas y trazas distribuidas +│ +├── Cosmos DB Account (cosmos-${token}) +│ └── Database: contentflow +│ ├── executor_catalog +│ ├── pipelines +│ ├── vaults +│ ├── vault_executions +│ ├── vault_exec_locks +│ ├── vault_crawl_checkpoints +│ └── pipeline_executions +│ +├── Storage Account (st${token}) +│ ├── Blob Container: content +│ └── Queue: contentflow-execution-requests +│ +├── Container Registry (cr${token}) +│ └── Imágenes Docker de los 3 servicios +│ +├── App Configuration Store (appcs-${token}) +│ └── 25+ claves de configuración +│ +├── Container Apps Environment (cae-${token}) +│ ├── Container App: api-${token} (Puerto 8090) +│ ├── Container App: worker-${token} (Puerto 8099) +│ └── Container App: web-${token} +│ +└── AI Foundry (opcional) + ├── AI Services (S0) + └── Deployments: + ├── gpt-4.1-mini (GlobalStandard, Cap: 100) + └── gpt-4.1 (GlobalStandard, Cap: 100) +``` + +### 6.2 SKUs y niveles de servicio + +| Recurso | SKU / Nivel | Detalles | +|---------|------------|----------| +| **Cosmos DB** | Serverless | Pago por consumo, sin capacidad reservada | +| **Storage Account** | Standard_LRS | Locally Redundant Storage, Hot tier | +| **Container Registry** | Standard | Premium si se usan endpoints privados | +| **App Configuration** | Standard | Configuración centralizada | +| **Log Analytics** | PerGB2018 | Pago por GB ingerido | +| **Application Insights** | Workspace-based | Conectado a Log Analytics | +| **AI Foundry / AI Services** | S0 | Tier estándar de servicios cognitivos | +| **GPT-4.1** | GlobalStandard | Capacidad: 100 TPM | +| **GPT-4.1-mini** | GlobalStandard | Capacidad: 100 TPM | +| **Container Apps** | Consumption | Serverless, perfil de carga de trabajo por consumo | +| **Managed Identity** | User-Assigned | N/A (sin SKU, es un recurso de identidad) | + +### 6.3 Módulos Bicep + +| Módulo | Archivo | Azure Verified Module (AVM) | Versión AVM | +|--------|---------|----------------------------|-------------| +| Identidad administrada | `user-assigned-identity.bicep` | `avm/res/managed-identity/user-assigned-identity` | 0.4.1 | +| Log Analytics | `log-analytics-ws.bicep` | Built-in | — | +| Application Insights | `app-insights.bicep` | Built-in | — | +| Cosmos DB | `cosmos.bicep` | `avm/res/document-db/database-account` | 0.16.0 | +| Storage Account | `storage.bicep` | `avm/res/storage/storage-account` | 0.27.1 | +| Container Registry | `container-registry.bicep` | `avm/res/container-registry` | Built-in | +| Container Apps Environment | `container-apps-environment.bicep` | `avm/res/app/managed-environment` | 0.11.3 | +| Container App | `container-app.bicep` | `avm/res/app/container-app` | 0.19.0 | +| App Configuration | `app-config-store.bicep` | Built-in | — | +| AI Foundry | `ai-foundry.bicep` | `avm/ptn/ai-ml/ai-foundry` | 0.5.0 | +| Static Web App | `static-web-app.bicep` | Built-in (no activo) | — | + +### 6.4 Cosmos DB — Estructura de base de datos + +``` +Cosmos DB Account (cosmos-${token}) +│ +├── Tipo: NoSQL (SQL API) +├── Consistencia: Session +├── Replicación: Región única (automaticFailover: false) +├── Backup: Continuous7Days (hasta 30 días de retención) +├── Autenticación: Solo Managed Identity (contraseñas deshabilitadas) +│ +└── Database: "contentflow" + │ + ├── executor_catalog (/id) + │ └── Definiciones de ejecutores del catálogo + │ + ├── pipelines (/id) + │ └── Configuración y YAML de cada pipeline + │ + ├── vaults (/id) + │ └── Definiciones de bóvedas de documentos + │ + ├── vault_executions (/id) + │ └── Registro de ejecuciones por vault + │ + ├── vault_exec_locks (/id) + │ └── Bloqueos distribuidos para evitar ejecuciones duplicadas + │ + ├── vault_crawl_checkpoints (/id) + │ └── Puntos de control para rastreo incremental + │ + └── pipeline_executions (/id) + └── Historial detallado de ejecuciones de pipelines +``` + +### 6.5 Storage Account — Blobs y colas + +``` +Storage Account (st${token}) +│ +├── Tipo: StorageV2 +├── SKU: Standard_LRS +├── Acceso: Hot tier +├── Autenticación: Solo Managed Identity (sharedKeyAccess: false) +├── Retención de eliminados: 7 días (blobs y contenedores) +│ +├── Blob Containers: +│ └── "content" (acceso público: None) +│ ├── Documentos originales +│ └── Contenido procesado +│ +└── Queues: + └── "contentflow-execution-requests" + └── Mensajes de tareas (InputSource y ContentProcessing) +``` + +### 6.6 Container Apps — Configuración de servicios + +| Configuración | API | Worker | Web | +|---------------|-----|--------|-----| +| **Puerto** | 8090 | 8099 | HTTP estándar | +| **CPU** | 1 core | 1 core | 1 core | +| **Memoria** | 2 GiB | 2 GiB | 2 GiB | +| **Réplicas mínimas** | 1 | 1 | 1 | +| **Réplicas máximas** | 2 | 2 | 2 | +| **Ingress** | Externo | Externo | Externo | +| **Escalado** | HTTP (10 concurrentes) | HTTP (10 concurrentes) | HTTP (10 concurrentes) | +| **Health check** | `/health` (60s intervalo) | — | — | +| **CORS** | GET, POST, PUT, DELETE, OPTIONS, PATCH | GET, POST, PUT, DELETE, OPTIONS, PATCH | GET, POST, PUT, DELETE, OPTIONS, PATCH | +| **Perfil** | Consumption | Consumption | Consumption | + +### 6.7 AI Foundry — Modelos de IA + +``` +AI Foundry +│ +├── AI Services Account (S0) +│ └── Nombre del proyecto: "contentflow-project" +│ +└── Model Deployments: + │ + ├── gpt-4.1-mini + │ ├── Formato: OpenAI + │ ├── Versión: 2025-04-14 + │ ├── SKU: GlobalStandard + │ └── Capacidad: 100 (TPM) + │ + └── gpt-4.1 + ├── Formato: OpenAI + ├── Versión: 2025-04-14 + ├── SKU: GlobalStandard + └── Capacidad: 100 (TPM) +``` + +### 6.8 Diagrama de infraestructura Azure + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ RESOURCE GROUP │ +│ │ +│ ┌─ Identidad ─────────────┐ ┌─ Monitoreo ────────────────────┐ │ +│ │ │ │ │ │ +│ │ Managed Identity │ │ Log Analytics App Insights │ │ +│ │ (User-Assigned) │ │ (PerGB2018) (Workspace) │ │ +│ │ │ │ │ │ +│ │ RBAC Roles: │ └─────────────────────────────────┘ │ +│ │ • Blob Data Contributor │ │ +│ │ • Queue Data Contributor│ ┌─ Configuración ────────────────┐ │ +│ │ • Cosmos DB Data Contrib│ │ │ │ +│ └──────────────────────────┘ │ App Configuration (Standard) │ │ +│ │ 25+ claves de configuración │ │ +│ ┌─ Cómputo ───────────────────┐│ │ │ +│ │ ││ Claves: │ │ +│ │ Container Apps Environment ││ • COSMOS_DB_ENDPOINT │ │ +│ │ (Consumption workload) ││ • BLOB_STORAGE_ACCOUNT_NAME │ │ +│ │ ││ • STORAGE_WORKER_QUEUE_URL │ │ +│ │ ┌──────┐ ┌──────┐ ┌──────┐ ││ • ... │ │ +│ │ │ API │ │Worker│ │ Web │ │└─────────────────────────────────┘ │ +│ │ │ 8090 │ │ 8099 │ │ 80 │ │ │ +│ │ │ 1CPU │ │ 1CPU │ │ 1CPU │ │ ┌─ IA ────────────────────────┐ │ +│ │ │ 2GiB │ │ 2GiB │ │ 2GiB │ │ │ │ │ +│ │ └──────┘ └──────┘ └──────┘ │ │ AI Foundry (S0) │ │ +│ │ │ │ ├── gpt-4.1-mini (100 TPM)│ │ +│ │ Container Registry (Std) │ │ └── gpt-4.1 (100 TPM) │ │ +│ │ (Imágenes Docker) │ │ │ │ +│ └──────────────────────────────┘ └─────────────────────────────┘ │ +│ │ +│ ┌─ Datos ──────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ Cosmos DB (Serverless) Storage Account (Standard_LRS) │ │ +│ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ +│ │ │ DB: contentflow │ │ Blob: "content" │ │ │ +│ │ │ ├── executor_catalog│ │ Queue: "contentflow- │ │ │ +│ │ │ ├── pipelines │ │ execution-requests" │ │ │ +│ │ │ ├── vaults │ └────────────────────────────┘ │ │ +│ │ │ ├── vault_executions│ │ │ +│ │ │ ├── vault_exec_locks│ │ │ +│ │ │ ├── vault_crawl_ │ │ │ +│ │ │ │ checkpoints │ │ │ +│ │ │ └── pipeline_ │ │ │ +│ │ │ executions │ │ │ +│ │ └─────────────────────┘ │ │ +│ └───────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 7. Interconexión entre componentes + +### 7.1 Flujo de datos completo + +``` +┌─────────┐ HTTP ┌─────────┐ Write ┌──────────┐ Poll ┌──────────┐ +│ WEB │────────▶│ API │────────▶│ QUEUE │────────▶│ WORKER │ +│ (React) │ REST │(FastAPI)│ Queue │ (Azure │ Recv │ (Python) │ +└─────────┘ └────┬────┘ │ Storage) │ └────┬─────┘ + │ └──────────┘ │ + │ │ + ┌────▼────┐ ┌────▼─────┐ + │COSMOS DB│◀─────────────────────────────│ PIPELINE │ + │(Metadat)│ Lee pipelines, actualiza │ ENGINE │ + └─────────┘ estados de ejecución │ (Lib) │ + │ └────┬─────┘ + ┌────▼────┐ │ + │ BLOB │◀───────────────────────────── │ + │STORAGE │ Lee documentos, guarda │ + │(Content)│ resultados procesados │ + └─────────┘ │ + ┌────▼─────┐ + │AZURE AI │ + │SERVICES │ + │(GPT, Doc │ + │ Intell.) │ + └──────────┘ +``` + +### 7.2 Flujo de mensajes por cola + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ FLUJO DE MENSAJES POR COLA │ +│ │ +│ ① API recibe solicitud de ejecución │ +│ POST /api/pipelines/{id}/execute │ +│ │ │ +│ ▼ │ +│ ② API crea registro de ejecución en Cosmos DB │ +│ PipelineExecution { status: "pending" } │ +│ │ │ +│ ▼ │ +│ ③ API envía InputSourceTask a la cola │ +│ ┌───────────────────────────────────────────┐ │ +│ │ Queue: contentflow-execution-requests │ │ +│ │ Message: { task_type: "INPUT_SOURCE", │ │ +│ │ payload: { pipeline_id, ... } } │ │ +│ └────────────────────┬──────────────────────┘ │ +│ │ │ +│ ④ Source Worker consume la tarea │ +│ │ │ +│ ▼ │ +│ ⑤ Source Worker ejecuta ejecutores de input │ +│ → Descubre N documentos │ +│ │ │ +│ ▼ │ +│ ⑥ Source Worker crea N tareas ContentProcessingTask │ +│ ┌───────────────────────────────────────────┐ │ +│ │ Queue: contentflow-execution-requests │ │ +│ │ Message × N: │ │ +│ │ { task_type: "CONTENT_PROCESSING", │ │ +│ │ payload: { pipeline_id, content, ... } } │ │ +│ └────────────────────┬──────────────────────┘ │ +│ │ │ +│ ⑦ Processing Workers consumen tareas (en paralelo) │ +│ │ │ +│ ▼ │ +│ ⑧ Cada worker ejecuta el pipeline completo sobre un documento │ +│ (excluyendo ejecutores de input) │ +│ │ │ +│ ▼ │ +│ ⑨ Resultados almacenados en Blob Storage / Cosmos DB │ +│ PipelineExecution { status: "completed" } │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 7.3 Flujo de autenticación + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ CADENA DE AUTENTICACIÓN │ +│ │ +│ User-Assigned Managed Identity │ +│ (id-${resourceToken}) │ +│ │ │ +│ ├── DefaultAzureCredential │ +│ │ (Cadena de credenciales automática) │ +│ │ │ +│ ├──▶ Cosmos DB ──── rol: "Cosmos DB SQL Data Contributor" │ +│ │ (Lectura/escritura de documentos) │ +│ │ │ +│ ├──▶ Blob Storage ─ rol: "Storage Blob Data Contributor" │ +│ │ (Lectura/escritura de blobs) │ +│ │ │ +│ ├──▶ Storage Queue ─ rol: "Storage Queue Data Contributor"│ +│ │ (Envío/recepción de mensajes) │ +│ │ │ +│ └──▶ App Config ──── rol: "App Configuration Data Reader" │ +│ (Lectura de configuración) │ +│ │ +│ Nota: Contraseñas y claves compartidas están DESHABILITADAS │ +│ (disableLocalAuthentication: true, allowSharedKeyAccess: false) │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### 7.4 Matriz de comunicación entre servicios + +| Origen | Destino | Protocolo | Propósito | +|--------|---------|-----------|-----------| +| **Web** → API | HTTP/REST | CRUD de pipelines, vaults, catálogo | +| **API** → Cosmos DB | SDK Azure (HTTPS) | Almacenar/leer metadatos | +| **API** → Blob Storage | SDK Azure (HTTPS) | Gestionar documentos | +| **API** → Storage Queue | SDK Azure (HTTPS) | Encolar tareas | +| **API** → App Configuration | SDK Azure (HTTPS) | Leer configuración | +| **Worker** → Storage Queue | SDK Azure (HTTPS) | Consumir tareas | +| **Worker** → Cosmos DB | SDK Azure (HTTPS) | Leer pipelines, actualizar estados | +| **Worker** → Blob Storage | SDK Azure (HTTPS) | Leer/escribir contenido | +| **Worker** → Azure AI Services | SDK Azure (HTTPS) | Procesamiento IA | +| **Worker** → App Configuration | SDK Azure (HTTPS) | Leer configuración | +| **Todos** → App Insights | SDK Azure (HTTPS) | Telemetría y trazas | + +### 7.5 Diagrama de secuencia: Ejecución de pipeline + +``` + Usuario Web (React) API (FastAPI) Cosmos DB Storage Queue Source Worker Processing Worker Azure AI + │ │ │ │ │ │ │ │ + │ ① Crear │ │ │ │ │ │ │ + │ pipeline │ │ │ │ │ │ │ + │───────────────▶│ │ │ │ │ │ │ + │ │ POST /pipeline │ │ │ │ │ │ + │ │───────────────▶│ │ │ │ │ │ + │ │ │──── save ────▶│ │ │ │ │ + │ │ │◀──── ok ──────│ │ │ │ │ + │ │◀───── 201 ─────│ │ │ │ │ │ + │◀───────────────│ │ │ │ │ │ │ + │ │ │ │ │ │ │ │ + │ ② Ejecutar │ │ │ │ │ │ │ + │───────────────▶│ │ │ │ │ │ │ + │ │ POST /execute │ │ │ │ │ │ + │ │───────────────▶│ │ │ │ │ │ + │ │ │── save exec ─▶│ │ │ │ │ + │ │ │── queue task ─┼─────────────▶│ │ │ │ + │ │◀─── 202 ──────│ │ │ │ │ │ + │◀───────────────│ │ │ │ │ │ │ + │ │ │ │ │ │ │ │ + │ │ │ │ │ ③ poll │ │ │ + │ │ │ │ │◀─────────────│ │ │ + │ │ │ │ │── InputTask──▶ │ │ + │ │ │ │◀─── read pipeline ──────────│ │ │ + │ │ │ │──── pipeline data ─────────▶│ │ │ + │ │ │ │ │ │ │ │ + │ │ │ │ │ ④ descubrir │ │ │ + │ │ │ │ │ N documentos│ │ │ + │ │ │ │ │◀── N tasks ──│ │ │ + │ │ │ │ │ │ │ │ + │ │ │ │ │ ⑤ poll │ │ │ + │ │ │ │ │◀─────────────┼───────────────│ │ + │ │ │ │ │─ ContentTask─┼──────────────▶│ │ + │ │ │ │◀────────── read pipeline ───┼───────────────│ │ + │ │ │ │ │ │ │ + │ │ │ │ │ │ ⑥ ejecutar │ │ + │ │ │ │ │ │ pipeline │ │ + │ │ │ │ │ │──────────────▶│──── AI ─────▶│ + │ │ │ │ │ │ │◀─── result ──│ + │ │ │ │◀──────────── update status ─┼───────────────│ │ + │ │ │ │ │ │ │ │ + │ ⑦ Consultar │ │ │ │ │ │ │ + │───────────────▶│ GET /executions│ │ │ │ │ │ + │ │───────────────▶│── query ─────▶│ │ │ │ │ + │ │ │◀── results ───│ │ │ │ │ + │ │◀──── 200 ──────│ │ │ │ │ │ + │◀───────────────│ │ │ │ │ │ │ +``` + +--- + +## 8. Modos de despliegue + +### 8.1 Modo básico + +``` +┌─────────────────────────────────────────────────────────────┐ +│ MODO BÁSICO │ +│ │ +│ Ideal para: Desarrollo, pruebas, demos │ +│ │ +│ Características: │ +│ ✓ Endpoints públicos (accesibles desde internet) │ +│ ✓ Todos los recursos creados nuevos │ +│ ✓ Red virtual creada automáticamente │ +│ ✓ Log Analytics + App Insights propios │ +│ ✗ Sin endpoints privados │ +│ ✗ Sin integración con redes existentes │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ INTERNET │ │ +│ │ │ │ │ +│ │ ┌──────────────────┼───────────────────┐ │ │ +│ │ │ Container Apps Environment │ │ │ +│ │ │ │ │ │ │ +│ │ │ ┌──────┐ ┌───┴───┐ ┌──────┐ │ │ │ +│ │ │ │ Web │ │ API │ │Worker│ │ │ │ +│ │ │ │(pub) │ │(pub) │ │(pub) │ │ │ │ +│ │ │ └──────┘ └───────┘ └──────┘ │ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ │ │ │ │ +│ │ ┌──────────────────┼───────────────────┐ │ │ +│ │ │ Recursos con acceso público │ │ │ +│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ +│ │ │ │CosmosDB│ │Storage │ │AppConf │ │ │ │ +│ │ │ │(public)│ │(public)│ │(public) │ │ │ │ +│ │ │ └────────┘ └────────┘ └────────┘ │ │ │ +│ │ └──────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 8.2 Modo AILZ (AI Landing Zone) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ MODO AILZ (AI Landing Zone) │ +│ │ +│ Ideal para: Producción empresarial │ +│ │ +│ Características: │ +│ ✓ Endpoints privados para todos los datos │ +│ ✓ Integración con VNet existente │ +│ ✓ Zonas DNS privadas │ +│ ✓ Shared Log Analytics + App Insights (del Landing Zone) │ +│ ✓ Container Registry Premium (endpoints privados) │ +│ ✓ Cumplimiento de seguridad empresarial │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ AI Landing Zone VNet (Existente) │ │ +│ │ │ │ +│ │ ┌─ aca-env-subnet ────────────────────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ Container Apps Environment (internal VNet) │ │ │ +│ │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ +│ │ │ │ Web │ │ API │ │Worker│ │ │ │ +│ │ │ └──────┘ └──────┘ └──────┘ │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─ pe-subnet (Private Endpoints) ─────────────────────┐ │ │ +│ │ │ │ │ │ +│ │ │ ┌────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ │ │ +│ │ │ │CosmosDB│ │Blob PE │ │Queue PE │ │ACR PE │ │ │ │ +│ │ │ │ PE │ │ │ │ │ │ │ │ │ │ +│ │ │ └────────┘ └────────┘ └──────────┘ └────────┘ │ │ │ +│ │ │ │ │ │ +│ │ └──────────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ Zonas DNS Privadas: │ │ +│ │ • *.documents.azure.com (Cosmos DB) │ │ +│ │ • *.blob.core.windows.net (Blob Storage) │ │ +│ │ • *.queue.core.windows.net (Storage Queue) │ │ +│ │ • *.azurecr.io (Container Registry) │ │ +│ │ • *.azconfig.io (App Configuration) │ │ +│ │ • *.cognitiveservices.azure.com (AI Services) │ │ +│ │ • Container Apps custom domain │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 8.3 Proceso de despliegue con azd + +``` +$ azd up +│ +├── ① pre-provision.sh +│ ├── Validar Azure CLI instalado +│ ├── Validar azd instalado +│ └── Validar Docker instalado +│ +├── ② Provisionar infraestructura (Bicep) +│ ├── Crear Managed Identity +│ ├── Crear Log Analytics + App Insights (básico) +│ ├── Crear Cosmos DB + containers +│ ├── Crear Storage Account + blob + queue +│ ├── Crear Container Registry +│ ├── Crear App Configuration + claves +│ ├── Crear Container Apps Environment +│ ├── Crear Container Apps (API, Worker, Web) +│ ├── Crear AI Foundry + deployments (opcional) +│ └── Asignar roles RBAC +│ +├── ③ Build y Push de imágenes Docker +│ ├── docker build contentflow-api/ +│ ├── docker build contentflow-worker/ +│ ├── docker build contentflow-web/ +│ └── docker push → Container Registry +│ +├── ④ Desplegar servicios en Container Apps +│ ├── Actualizar imagen del API +│ ├── Actualizar imagen del Worker +│ └── Actualizar imagen del Web +│ +├── ⑤ post-deploy-api.sh +│ └── Configuración post-despliegue del API +│ +└── ⑥ post-deploy.sh + └── Tareas finales interactivas + +Resultado: URLs de los servicios desplegados + → API: https://api-xxxx.azurecontainerapps.io + → Worker: https://worker-xxxx.azurecontainerapps.io + → Web: https://web-xxxx.azurecontainerapps.io +``` + +### 8.4 Diagrama comparativo de modos + +``` + MODO BÁSICO MODO AILZ + ┌─────────────────────┐ ┌─────────────────────────┐ + │ │ │ │ + │ ☁️ Internet │ │ ☁️ Internet (limitado) │ + │ │ │ │ │ │ + │ ┌────▼────┐ │ │ ┌────▼────┐ │ + │ │Container│ │ │ │Container│ │ + │ │Apps │ │ │ │Apps │ │ + │ │(público)│ │ │ │(VNet │ │ + │ └────┬────┘ │ │ │internal)│ │ + │ │ │ │ └────┬────┘ │ + │ ┌────▼───┐ │ │ ┌────▼────┐ │ + │ │Recursos│ │ │ │ Private │ │ + │ │público │ │ │ │Endpoints│ │ + │ │acceso │ │ │ │(VNet) │ │ + │ └────────┘ │ │ └─────────┘ │ + │ │ │ │ + │ ✅ Rápido │ │ ✅ Seguro │ + │ ✅ Simple │ │ ✅ Empresarial │ + │ ❌ No para prod. │ │ ❌ Requiere VNet/LZ │ + └─────────────────────┘ └─────────────────────────┘ +``` + +--- + +## 9. Seguridad y autenticación + +### 9.1 Modelo de identidad + +ContentFlow utiliza **Azure Managed Identity** (identidad administrada asignada por el usuario) para toda la autenticación entre servicios. No se utilizan contraseñas, claves de acceso ni cadenas de conexión. + +``` +┌──────────────────────────────────────────────────────┐ +│ USER-ASSIGNED MANAGED IDENTITY │ +│ (id-${resourceToken}) │ +│ │ +│ Utilizada por: │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ API │ │ Worker │ │ Web │ │ +│ │ Service │ │ Service │ │ (build) │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ Autenticación en código: │ +│ DefaultAzureCredential() → cadena automática: │ +│ 1. Environment variables │ +│ 2. Managed Identity │ +│ 3. Azure CLI (desarrollo local) │ +│ 4. Azure PowerShell │ +└──────────────────────────────────────────────────────┘ +``` + +### 9.2 Roles RBAC asignados + +| Recurso | Rol RBAC | Asignado a | Permiso | +|---------|----------|-----------|---------| +| **Cosmos DB** | Cosmos DB SQL Data Contributor | Managed Identity | Leer/escribir datos | +| **Cosmos DB** | Cosmos DB SQL Data Contributor | Deployer (principal) | Leer/escribir datos | +| **Blob Storage** | Storage Blob Data Contributor | Managed Identity | Leer/escribir blobs | +| **Blob Storage** | Storage Blob Data Contributor | Deployer (principal) | Leer/escribir blobs | +| **Storage Queue** | Storage Queue Data Contributor | Managed Identity | Enviar/recibir mensajes | +| **Storage Queue** | Storage Queue Data Contributor | Deployer (principal) | Enviar/recibir mensajes | +| **App Configuration** | App Configuration Data Reader | Managed Identity | Leer configuración | +| **Container Registry** | AcrPull | Managed Identity | Descargar imágenes | + +### 9.3 Diagrama de seguridad + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ MODELO DE SEGURIDAD │ +│ │ +│ ┌─ Autenticación ─────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ Managed Identity (sin contraseñas) │ │ +│ │ ✅ DefaultAzureCredential (cadena automática) │ │ +│ │ ✅ disableLocalAuthentication: true (Cosmos DB) │ │ +│ │ ✅ allowSharedKeyAccess: false (Storage) │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Autorización ──────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ RBAC (Role-Based Access Control) para cada recurso │ │ +│ │ ✅ Principio de mínimo privilegio │ │ +│ │ ✅ Roles específicos por tipo de operación │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Red (modo AILZ) ──────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ Private Endpoints para datos │ │ +│ │ ✅ Subnets dedicadas │ │ +│ │ ✅ DNS privado │ │ +│ │ ✅ Network rules: Deny por defecto │ │ +│ │ ✅ Bypass: AzureServices │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ Datos ─────────────────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ✅ Encriptación en tránsito (HTTPS/TLS) │ │ +│ │ ✅ Encriptación en reposo (Azure managed keys) │ │ +│ │ ✅ Backup continuo (Cosmos DB: 7-30 días) │ │ +│ │ ✅ Soft delete (Blob Storage: 7 días retención) │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 10. Configuración centralizada + +### 10.1 Azure App Configuration + +Todas las configuraciones de runtime se almacenan en **Azure App Configuration** y se leen tanto por el API como por el Worker: + +``` +App Configuration Store (appcs-${token}) +│ +├── Cosmos DB +│ ├── COSMOS_DB_ENDPOINT: https://cosmos-xxx.documents.azure.com +│ ├── COSMOS_DB_NAME: contentflow +│ ├── COSMOS_DB_CONTAINER_PIPELINES: pipelines +│ ├── COSMOS_DB_CONTAINER_VAULTS: vaults +│ ├── COSMOS_DB_CONTAINER_VAULT_EXECUTIONS: vault_executions +│ ├── COSMOS_DB_CONTAINER_VAULT_EXECUTION_LOCKS: vault_exec_locks +│ ├── COSMOS_DB_CONTAINER_CRAWL_CHECKPOINTS: vault_crawl_checkpoints +│ └── COSMOS_DB_CONTAINER_PIPELINE_EXECUTIONS: pipeline_executions +│ +├── Storage +│ ├── BLOB_STORAGE_ACCOUNT_NAME: stxxxx +│ ├── BLOB_STORAGE_CONTAINER_NAME: content +│ ├── STORAGE_ACCOUNT_WORKER_QUEUE_URL: https://stxxxx.queue.core.windows.net +│ └── STORAGE_WORKER_QUEUE_NAME: contentflow-execution-requests +│ +├── Worker +│ ├── WORKER_ENGINE_API_ENDPOINT: https://worker-xxx.azurecontainerapps.io +│ ├── NUM_PROCESSING_WORKERS: 0 +│ └── NUM_SOURCE_WORKERS: 1 +│ +└── Application + ├── API_SERVER_PORT: 8090 + ├── LOG_LEVEL: DEBUG + └── DEBUG: true +``` + +### 10.2 Variables de entorno por servicio + +| Variable | API | Worker | Descripción | +|----------|:---:|:------:|-------------| +| `COSMOS_DB_ENDPOINT` | ✅ | ✅ | Endpoint de Cosmos DB | +| `COSMOS_DB_NAME` | ✅ | ✅ | Nombre de la base de datos | +| `BLOB_STORAGE_ACCOUNT_NAME` | ✅ | ✅ | Nombre de la cuenta de storage | +| `BLOB_STORAGE_CONTAINER_NAME` | ✅ | ✅ | Nombre del contenedor blob | +| `STORAGE_ACCOUNT_WORKER_QUEUE_URL` | ✅ | ✅ | URL de la cola | +| `STORAGE_WORKER_QUEUE_NAME` | ✅ | ✅ | Nombre de la cola | +| `API_SERVER_PORT` | ✅ | — | Puerto del API (8090) | +| `API_PORT` | — | ✅ | Puerto de health del worker (8099) | +| `NUM_PROCESSING_WORKERS` | — | ✅ | Workers de procesamiento | +| `NUM_SOURCE_WORKERS` | — | ✅ | Workers de entrada | +| `QUEUE_POLL_INTERVAL_SECONDS` | — | ✅ | Intervalo de poll (5s) | +| `DEFAULT_POLLING_INTERVAL_SECONDS` | — | ✅ | Intervalo de descubrimiento (300s) | +| `MAX_TASK_RETRIES` | — | ✅ | Reintentos por tarea (3) | +| `TASK_TIMEOUT_SECONDS` | — | ✅ | Timeout por tarea (600s) | +| `LOG_LEVEL` | ✅ | ✅ | Nivel de logging (DEBUG) | +| `DEBUG` | ✅ | ✅ | Modo debug | + +--- + +## 11. Monitoreo y observabilidad + +``` +┌─────────────────────────────────────────────────────────────┐ +│ STACK DE OBSERVABILIDAD │ +│ │ +│ ┌─ Application Insights ─────────────────────────────────┐ │ +│ │ │ │ +│ │ • Trazas distribuidas entre servicios │ │ +│ │ • Métricas de rendimiento (latencia, errores) │ │ +│ │ • Logs de aplicación (Python + React) │ │ +│ │ • Mapa de dependencias entre componentes │ │ +│ │ • Alertas configurables │ │ +│ │ │ │ +│ └─────────────────────┬───────────────────────────────────┘ │ +│ │ │ +│ ┌─ Log Analytics ─────▼──────────────────────────────────┐ │ +│ │ │ │ +│ │ • Logs de Container Apps (stdout/stderr) │ │ +│ │ • Logs de sistema de Container Apps Environment │ │ +│ │ • Métricas de infraestructura │ │ +│ │ • Consultas KQL personalizables │ │ +│ │ • Retención configurable │ │ +│ │ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ Fuentes de datos: │ +│ ├── API Service ──────▶ Logs HTTP, latencia, errores │ +│ ├── Worker Service ───▶ Logs de procesamiento, tareas │ +│ ├── Web Service ──────▶ Logs de build, errores client-side │ +│ └── Container Apps ───▶ Escalado, arranques, health checks │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 12. Resumen de dependencias entre componentes + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ MAPA DE DEPENDENCIAS DE CONTENTFLOW │ +│ │ +│ │ +│ contentflow-web ──────────────▶ contentflow-api │ +│ (Frontend) HTTP/REST (Backend) │ +│ │ │ +│ ├──── azure-cosmos (SDK) │ +│ ├──── azure-storage-blob (SDK) │ +│ ├──── azure-storage-queue (SDK) │ +│ ├──── azure-appconfiguration (SDK) │ +│ └──── contentflow-lib (local) │ +│ │ │ +│ contentflow-worker ─────────────────────────────┤ │ +│ (Processing) │ │ +│ │ │ │ +│ ├──── azure-cosmos (SDK) │ │ +│ ├──── azure-storage-queue (SDK) contentflow-lib │ +│ ├──── azure-storage-blob (SDK) (Core Engine) │ +│ └──── contentflow-lib (local) ──┐ │ │ +│ │ ├── Pipeline Engine │ +│ └──────────├── 35+ Ejecutores │ +│ ├── Content Model │ +│ ├── Conectores Azure │ +│ │ ├── Blob Storage │ +│ │ ├── AI Search │ +│ │ ├── Document Intel. │ +│ │ ├── OpenAI │ +│ │ └── Cosmos DB │ +│ └── Utilidades │ +│ │ +│ infra/ (Bicep) │ +│ │ │ +│ └──── Aprovisiona TODOS los recursos de Azure que los │ +│ servicios anteriores consumen │ +│ │ +│ azure.yaml │ +│ │ │ +│ └──── Orquesta el despliegue de infra/ + servicios │ +│ via Azure Developer CLI (azd) │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +> **Este documento refleja el estado de la arquitectura a la fecha de generación. Consulte el código fuente para la información más actualizada.** diff --git a/Analysis/03-use_cases_examples.md b/Analysis/03-use_cases_examples.md new file mode 100644 index 0000000..d2e641f --- /dev/null +++ b/Analysis/03-use_cases_examples.md @@ -0,0 +1,666 @@ +# ContentFlow — Casos de Uso y Ejemplos + +> **Catálogo de escenarios implementables con el acelerador ContentFlow**, organizados por industria y función, con los ejecutores recomendados para cada caso. + +--- + +## Índice + +- [1. Procesamiento de facturas y recibos](#1-procesamiento-de-facturas-y-recibos) +- [2. Indexación inteligente de contenido](#2-indexación-inteligente-de-contenido) +- [3. Análisis de contratos](#3-análisis-de-contratos) +- [4. Grafos de conocimiento evolutivos](#4-grafos-de-conocimiento-evolutivos) +- [5. Procesamiento de formularios](#5-procesamiento-de-formularios) +- [6. Cumplimiento normativo y gestión de riesgos](#6-cumplimiento-normativo-y-gestión-de-riesgos) +- [7. Procesamiento de contenido web](#7-procesamiento-de-contenido-web) +- [8. Análisis de imagen y contenido visual](#8-análisis-de-imagen-y-contenido-visual) +- [9. Procesamiento de adjuntos de correo electrónico](#9-procesamiento-de-adjuntos-de-correo-electrónico) +- [10. Ingestión de contenido para GPT-RAG](#10-ingestión-de-contenido-para-gpt-rag) +- [11. Resumen de artículos y reportes](#11-resumen-de-artículos-y-reportes) +- [12. Traducción de contenido multilingüe](#12-traducción-de-contenido-multilingüe) +- [13. Resumen de ejecutores por caso de uso](#13-resumen-de-ejecutores-por-caso-de-uso) +- [14. Pipelines de ejemplo incluidos en el repositorio](#14-pipelines-de-ejemplo-incluidos-en-el-repositorio) + +--- + +## 1. Procesamiento de facturas y recibos + +Automatiza la extracción de datos estructurados de facturas, recibos y órdenes de compra usando Azure Content Understanding. + +**Capacidades**: +- Extraer datos estructurados de facturas usando Azure Content Understanding +- Clasificar documentos por tipo (factura, recibo, orden de compra) +- Validar campos extraídos y señalar anomalías +- Exportar a sistemas ERP o bases de datos + +**Pipeline sugerido**: + +```yaml +name: procesamiento-facturas +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: facturas-entrantes + file_extensions: [".pdf", ".png", ".jpg"] + + - executor: azure_blob_content_retriever + + - executor: azure_content_understanding_extractor + settings: + model_id: prebuilt-invoice + + - executor: content_classifier + settings: + categories: ["factura", "recibo", "orden_de_compra"] + + - executor: field_mapper + settings: + mappings: + vendor_name: "proveedor" + total_amount: "monto_total" + invoice_date: "fecha_factura" + + - executor: azure_blob_output + settings: + blob_container_name: facturas-procesadas +``` + +**Ejecutores clave**: `azure_content_understanding_extractor`, `content_classifier`, `field_mapper`, `azure_blob_output` + +--- + +## 2. Indexación inteligente de contenido + +Procesa documentos, imágenes, audio y video para generar embeddings vectoriales e indexar en Azure AI Search, habilitando búsqueda semántica empresarial. + +**Capacidades**: +- Procesar documentos en múltiples formatos (PDF, Word, Excel, PowerPoint) +- Generar vectores de embedding para búsqueda semántica +- Indexar contenido en Azure AI Search +- Habilitar búsqueda semántica en todo el contenido empresarial + +**Pipeline sugerido**: + +```yaml +name: indexacion-inteligente +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documentos-empresa + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + settings: + model_id: prebuilt-layout + + - executor: recursive_text_chunker + settings: + chunk_size: 1000 + chunk_overlap: 200 + + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + - executor: ai_search_index_output + settings: + index_name: contenido-empresarial +``` + +**Ejecutores clave**: `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `ai_search_index_output` + +--- + +## 3. Análisis de contratos + +Extrae cláusulas clave, identifica partes involucradas, fechas, montos, y genera resúmenes automáticos para revisión legal. + +**Capacidades**: +- Extraer cláusulas clave y obligaciones contractuales +- Identificar partes, fechas y valores monetarios +- Señalar términos riesgosos usando análisis de IA +- Generar resúmenes para revisión legal + +**Pipeline sugerido**: + +```yaml +name: analisis-contratos +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: contratos + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: entity_extractor + settings: + entity_types: ["persona", "organizacion", "fecha", "monto"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1 + prompt: | + Analiza este contrato y extrae: + 1. Cláusulas clave y obligaciones + 2. Términos riesgosos o inusuales + 3. Fechas límite y vencimientos + 4. Valores monetarios y condiciones de pago + + - executor: text_summarizer + settings: + max_length: 500 + style: "resumen ejecutivo legal" + + - executor: azure_blob_output + settings: + blob_container_name: contratos-analizados +``` + +**Ejecutores clave**: `pdf_extractor`, `entity_extractor`, `azure_openai_agent`, `text_summarizer` + +--- + +## 4. Grafos de conocimiento evolutivos + +Construye grafos de conocimiento que mapean entidades, relaciones y estructuras organizativas a partir de documentos corporativos. + +**Capacidades**: +- Extraer entidades (personas, organizaciones, productos, ubicaciones) +- Identificar relaciones (trabaja_en, gestiona, ubicado_en) +- Mapear estructuras organizativas +- Descubrir conexiones ocultas entre documentos + +**Pipeline sugerido**: + +```yaml +name: grafo-conocimiento +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documentos-corporativos + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + + - executor: recursive_text_chunker + settings: + chunk_size: 2000 + + - executor: entity_extractor + settings: + entity_types: ["persona", "organizacion", "producto", "ubicacion"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1 + prompt: | + A partir de las entidades extraídas, identifica relaciones como: + - trabaja_en, gestiona, reporta_a + - ubicado_en, opera_en + - provee, compra_a, compite_con + Devuelve las relaciones en formato estructurado. + + - executor: azure_blob_output + settings: + blob_container_name: grafos-conocimiento +``` + +**Ejecutores clave**: `entity_extractor`, `azure_openai_agent`, `document_set_initializer`, `cross_document_comparison` + +--- + +## 5. Procesamiento de formularios + +Automatiza la captura y validación de datos de formularios, solicitudes, reclamaciones y aplicaciones. + +**Capacidades**: +- Procesar solicitudes, reclamaciones y formularios +- Extraer campos estructurados con ejecutores de IA +- Validar completitud y precisión de los datos +- Enrutar al sistema de línea de negocio (LoB) apropiado + +**Pipeline sugerido**: + +```yaml +name: procesamiento-formularios +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: formularios-entrantes + + - executor: azure_blob_content_retriever + + - executor: azure_content_understanding_extractor + settings: + model_id: prebuilt-document + + - executor: field_selector + settings: + fields: ["nombre", "fecha", "tipo_solicitud", "monto", "descripcion"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1-mini + prompt: | + Valida los campos extraídos: + - ¿Están todos los campos obligatorios presentes? + - ¿Los formatos son correctos (fechas, montos)? + - Señala campos faltantes o inconsistentes. + + - executor: content_classifier + settings: + categories: ["solicitud_credito", "reclamacion_seguro", "solicitud_empleo"] + + - executor: azure_blob_output + settings: + blob_container_name: formularios-procesados +``` + +**Ejecutores clave**: `azure_content_understanding_extractor`, `field_selector`, `content_classifier`, `azure_openai_agent` + +--- + +## 6. Cumplimiento normativo y gestión de riesgos + +Detecta información personal identificable (PII), clasifica documentos por sensibilidad y señala problemas de cumplimiento. + +**Capacidades**: +- Detectar PII (Información Personalmente Identificable) +- Clasificar documentos por nivel de sensibilidad +- Señalar problemas de cumplimiento normativo +- Extraer entidades clave y temas sensibles + +**Pipeline sugerido**: + +```yaml +name: cumplimiento-riesgos +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: documentos-revision + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: pii_detector + settings: + pii_types: ["nombre", "email", "telefono", "numero_seguro_social", + "tarjeta_credito", "direccion", "fecha_nacimiento"] + + - executor: content_classifier + settings: + categories: ["publico", "interno", "confidencial", "restringido"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1 + prompt: | + Analiza el documento para cumplimiento normativo: + - ¿Contiene información regulada (GDPR, HIPAA, PCI-DSS)? + - ¿Se manejan datos sensibles correctamente? + - Identifica riesgos potenciales de cumplimiento. + Genera un reporte con nivel de riesgo: bajo, medio, alto, crítico. + + - executor: azure_blob_output + settings: + blob_container_name: reportes-cumplimiento +``` + +**Ejecutores clave**: `pii_detector`, `content_classifier`, `entity_extractor`, `azure_openai_agent` + +--- + +## 7. Procesamiento de contenido web + +Extrae, traduce y corrige contenido de sitios web para alimentar soluciones de chat basadas en RAG o sitios multilingües. + +**Capacidades**: +- Extraer contenido de sitios web para soluciones de chat basadas en RAG +- Traducir contenido web para experiencias multilingües +- Identificar y corregir errores ortográficos y de contenido + +**Pipeline sugerido**: + +```yaml +name: procesamiento-web +steps: + - executor: web_scraper + settings: + urls: + - "https://docs.empresa.com" + - "https://soporte.empresa.com" + max_depth: 3 + selectors: ["article", "main", ".content"] + + - executor: recursive_text_chunker + settings: + chunk_size: 1500 + + - executor: language_detector + + - executor: content_translator + settings: + target_language: "es" + + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + - executor: ai_search_index_output + settings: + index_name: contenido-web-rag +``` + +**Ejecutores clave**: `web_scraper`, `language_detector`, `content_translator`, `azure_openai_embeddings` + +--- + +## 8. Análisis de imagen y contenido visual + +Digitaliza notas clínicas manuscritas, extrae texto de reportes visuales y analiza imágenes de daños para procesamiento automatizado de reclamaciones. + +**Capacidades**: + +| Industria | Aplicación | +|-----------|-----------| +| **Salud** | Digitalizar notas clínicas manuscritas, extraer texto de reportes, generar descripciones de imágenes médicas para registros electrónicos de salud y cumplimiento de accesibilidad | +| **Seguros** | Procesar formularios manuscritos de reclamaciones, extraer datos de fotos de accidentes, analizar imágenes de evaluación de daños para procesamiento automatizado | + +**Pipeline sugerido**: + +```yaml +name: analisis-visual-seguros +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: reclamaciones-fotos + file_extensions: [".jpg", ".png", ".pdf"] + + - executor: azure_blob_content_retriever + + - executor: azure_content_understanding_extractor + settings: + model_id: prebuilt-document + + - executor: azure_openai_agent + settings: + model: gpt-4.1 + prompt: | + Analiza esta imagen/documento de reclamación de seguro: + - Identifica el tipo de daño visible + - Estima la severidad (leve, moderado, severo) + - Extrae información del formulario de reclamación + - Señala cualquier inconsistencia + + - executor: content_classifier + settings: + categories: ["vehicular", "propiedad", "salud", "vida"] + + - executor: azure_blob_output + settings: + blob_container_name: reclamaciones-procesadas +``` + +**Ejecutores clave**: `azure_content_understanding_extractor`, `azure_openai_agent`, `content_classifier` + +--- + +## 9. Procesamiento de adjuntos de correo electrónico + +Analiza hilos de correo para extraer tareas, detectar sentimiento y enrutar automáticamente a los equipos adecuados. + +**Capacidades**: + +| Industria | Aplicación | +|-----------|-----------| +| **Servicio al cliente** | Analizar hilos de soporte para extraer acciones pendientes, detectar sentimiento del cliente, y enrutar automáticamente problemas urgentes a los equipos apropiados | +| **Legal** | Categorizar automáticamente correspondencia legal, identificar puntos de negociación, extraer fechas límite y obligaciones de hilos de correo | + +**Pipeline sugerido**: + +```yaml +name: procesamiento-correos +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: correos-entrantes + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: sentiment_analyser + settings: + granularity: "documento" + + - executor: entity_extractor + settings: + entity_types: ["persona", "fecha", "organizacion", "accion"] + + - executor: azure_openai_agent + settings: + model: gpt-4.1-mini + prompt: | + Del correo electrónico, extrae: + 1. Acciones pendientes y responsables + 2. Fechas límite mencionadas + 3. Nivel de urgencia (bajo, medio, alto, crítico) + 4. Tema principal y categoría + + - executor: content_classifier + settings: + categories: ["soporte_tecnico", "facturacion", "queja", + "solicitud_informacion", "legal"] + + - executor: azure_blob_output + settings: + blob_container_name: correos-procesados +``` + +**Ejecutores clave**: `sentiment_analyser`, `entity_extractor`, `azure_openai_agent`, `content_classifier` + +--- + +## 10. Ingestión de contenido para GPT-RAG + +Construye bases de conocimiento buscables a partir de documentos empresariales, procesando múltiples formatos y creando embeddings para búsqueda semántica en soluciones de chat con IA. + +**Capacidades**: +- Construir bases de conocimiento buscables desde documentos empresariales +- Procesar contenido en múltiples formatos para chat impulsado por IA +- Crear embeddings para búsqueda semántica + +**Pipeline sugerido**: + +```yaml +name: ingestion-gpt-rag +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: base-conocimiento + + - executor: azure_blob_content_retriever + + - executor: azure_document_intelligence_extractor + settings: + model_id: prebuilt-layout + + - executor: recursive_text_chunker + settings: + chunk_size: 800 + chunk_overlap: 150 + + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + - executor: gptrag_search_index_document_generator + settings: + index_name: rag-knowledge-base + + - executor: ai_search_index_output + settings: + index_name: rag-knowledge-base +``` + +**Ejecutores clave**: `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `gptrag_search_index_document_generator` + +--- + +## 11. Resumen de artículos y reportes + +Genera resúmenes concisos de artículos de noticias, reportes ejecutivos y snippets para redes sociales. + +**Capacidades**: +- Resumir artículos de noticias para boletines internos +- Crear resúmenes ejecutivos de reportes extensos +- Generar snippets para publicaciones en redes sociales + +**Pipeline sugerido**: + +```yaml +name: resumen-articulos +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: articulos-reportes + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: language_detector + + - executor: keyword_extractor + settings: + max_keywords: 10 + + - executor: text_summarizer + settings: + max_length: 300 + style: "resumen ejecutivo" + + - executor: azure_openai_agent + settings: + model: gpt-4.1-mini + prompt: | + A partir del resumen generado, crea: + 1. Un titular de una línea + 2. Un resumen de 3 oraciones para boletín + 3. Un snippet para redes sociales (max 280 caracteres) + + - executor: azure_blob_output + settings: + blob_container_name: resumenes-generados +``` + +**Ejecutores clave**: `keyword_extractor`, `text_summarizer`, `language_detector`, `azure_openai_agent` + +--- + +## 12. Traducción de contenido multilingüe + +Traduce documentación de productos, marketing y bases de conocimiento para alcanzar audiencias globales. + +**Capacidades**: +- Traducir documentación de productos a múltiples idiomas +- Localizar contenido de marketing para diferentes regiones +- Crear bases de conocimiento multilingües + +**Pipeline sugerido**: + +```yaml +name: traduccion-multilingue +steps: + - executor: azure_blob_input_discovery + settings: + blob_container_name: contenido-original + + - executor: azure_blob_content_retriever + + - executor: pdf_extractor + + - executor: language_detector + + - executor: recursive_text_chunker + settings: + chunk_size: 2000 + + - executor: content_translator + settings: + target_languages: ["es", "fr", "de", "pt", "ja"] + + - executor: azure_openai_embeddings + settings: + model: text-embedding-3-small + + - executor: ai_search_index_output + settings: + index_name: contenido-multilingue +``` + +**Ejecutores clave**: `language_detector`, `content_translator`, `azure_openai_embeddings`, `ai_search_index_output` + +--- + +## 13. Resumen de ejecutores por caso de uso + +| Caso de uso | Ejecutores principales | +|---|---| +| **Facturas y recibos** | `azure_content_understanding_extractor`, `content_classifier`, `field_mapper` | +| **Indexación inteligente** | `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `ai_search_index_output` | +| **Análisis de contratos** | `pdf_extractor`, `entity_extractor`, `azure_openai_agent`, `text_summarizer` | +| **Grafos de conocimiento** | `entity_extractor`, `azure_openai_agent`, `cross_document_comparison` | +| **Formularios** | `azure_content_understanding_extractor`, `field_selector`, `content_classifier` | +| **Cumplimiento y riesgos** | `pii_detector`, `content_classifier`, `azure_openai_agent` | +| **Contenido web** | `web_scraper`, `content_translator`, `azure_openai_embeddings` | +| **Análisis visual** | `azure_content_understanding_extractor`, `azure_openai_agent` | +| **Correo electrónico** | `sentiment_analyser`, `entity_extractor`, `content_classifier` | +| **GPT-RAG** | `azure_document_intelligence_extractor`, `recursive_text_chunker`, `gptrag_search_index_document_generator` | +| **Resúmenes** | `keyword_extractor`, `text_summarizer`, `azure_openai_agent` | +| **Traducción** | `language_detector`, `content_translator`, `azure_openai_embeddings` | + +--- + +## 14. Pipelines de ejemplo incluidos en el repositorio + +El repositorio incluye más de **20 pipelines de ejemplo** listos para ejecutar en la carpeta `contentflow-lib/samples/`: + +| Carpeta | Nombre | Descripción | +|---------|--------|-------------| +| `01-simple/` | Pipeline simple | Pipeline básico de extracción de texto | +| `02-batch-processing/` | Procesamiento por lotes | Procesamiento de múltiples documentos | +| `03-pdf-extractor_chunker/` | PDF + Chunking | Extracción de PDF y división en fragmentos | +| `04-word-extractor/` | Extractor Word | Procesamiento de documentos .docx | +| `05-powerpoint-extractor/` | Extractor PowerPoint | Procesamiento de presentaciones .pptx | +| `06-ai-analysis/` | Análisis con IA | Análisis inteligente con GPT | +| `07-embeddings/` | Embeddings | Generación de vectores de embedding | +| `08-content-understanding/` | Content Understanding | Azure Content Understanding | +| `09-blob-input/` | Entrada desde Blob | Descubrimiento de contenido en Blob Storage | +| `10-table-row-splitter/` | Divisor de filas | Procesamiento fila por fila de tablas | +| `11-excel-extractor/` | Extractor Excel | Procesamiento de hojas de cálculo | +| `12-field-transformation/` | Transformación de campos | Mapeo y transformación de datos | +| `13-blob-output-sample/` | Salida a Blob | Escritura de resultados a Blob Storage | +| `14-gpt-rag-ingestion/` | Ingestión GPT-RAG | Pipeline completo para RAG | +| `15-document-analysis/` | Análisis de documentos | Análisis completo con Document Intelligence | +| `16-spreadsheet-pipeline/` | Pipeline de hojas de cálculo | Procesamiento de Excel end-to-end | +| `17-knowledge-graph/` | Grafo de conocimiento | Extracción de entidades y relaciones | +| `18-web-scraping/` | Web scraping | Extracción de contenido web | +| `19-sub-pipelines/` | Sub-pipelines | Pipelines anidados | +| `20-document-set-static/` | Conjunto estático | Procesamiento de conjuntos de documentos | +| `21-document-set-comparison/` | Comparación de documentos | Comparación cruzada de documentos | +| `22-document-set-dynamic/` | Conjunto dinámico | Conjuntos de documentos dinámicos | +| `23-inline-document-set/` | Conjunto inline | Conjuntos definidos en línea | +| `27-subpipeline-processing/` | Sub-pipeline avanzado | Procesamiento con sub-pipelines | +| `28-advanced-batch/` | Lotes avanzado | Procesamiento por lotes avanzado | +| `32-parallel-processing/` | Procesamiento paralelo | Flujos de trabajo en paralelo | +| `44-conditional-routing/` | Enrutamiento condicional | Lógica condicional en pipelines | + +--- + +> Cada caso de uso se puede implementar combinando los **35+ ejecutores** disponibles en ContentFlow. Los pipelines YAML son completamente configurables y se pueden diseñar visualmente desde la interfaz web o editarse directamente en el editor YAML integrado. diff --git a/Analysis/04-ai_lz_options.md b/Analysis/04-ai_lz_options.md new file mode 100644 index 0000000..07aeb4d --- /dev/null +++ b/Analysis/04-ai_lz_options.md @@ -0,0 +1,1224 @@ +# Guía de Despliegue: ContentFlow en Modo AI Landing Zone Integrated + +> **Documento técnico operativo** - Instrucciones paso a paso para desplegar ContentFlow con conectividad privada usando una AI Landing Zone mínima en un ambiente de demo con suscripción única. + +--- + +## Tabla de Contenidos + +1. [Resumen Ejecutivo](#1-resumen-ejecutivo) +2. [¿Qué es el Modo AILZ-Integrated?](#2-qué-es-el-modo-ailz-integrated) +3. [Arquitectura de Red: Basic vs AILZ](#3-arquitectura-de-red-basic-vs-ailz) +4. [Prerrequisitos Generales](#4-prerrequisitos-generales) +5. [El Problema: Demo con Suscripción Única](#5-el-problema-demo-con-suscripción-única) +6. [Solución: AI Landing Zone Mínima ("Mini-AILZ")](#6-solución-ai-landing-zone-mínima-mini-ailz) +7. [Paso a Paso: Crear la Mini-AILZ](#7-paso-a-paso-crear-la-mini-ailz) +8. [Paso a Paso: Desplegar ContentFlow en Modo AILZ](#8-paso-a-paso-desplegar-contentflow-en-modo-ailz) +9. [Acceso y Pruebas de Conectividad Privada](#9-acceso-y-pruebas-de-conectividad-privada) +10. [Mapa Completo de Parámetros](#10-mapa-completo-de-parámetros) +11. [Cambios por Recurso en Modo AILZ](#11-cambios-por-recurso-en-modo-ailz) +12. [Troubleshooting](#12-troubleshooting) +13. [Costos Estimados de la Mini-AILZ](#13-costos-estimados-de-la-mini-ailz) +14. [Limpieza de Recursos](#14-limpieza-de-recursos) + +--- + +## 1. Resumen Ejecutivo + +ContentFlow soporta dos modos de despliegue: + +| Aspecto | `basic` | `ailz-integrated` | +|---|---|---| +| Endpoints | Públicos (internet) | Privados (VNet) | +| Red | Sin VNet | VNet + Private Endpoints | +| DNS | Resolución pública | Private DNS Zones | +| ACR SKU | Standard | **Premium** (requerido para PE) | +| Firewall de recursos | `Allow` (todo abierto) | `Deny` (solo PE) | +| Ingress de Container Apps | Externo (público) | **Interno** (solo VNet) | +| Acceso a la UI Web | Navegador directo | Requiere VPN/JumpBox | + +**Escenario de este documento:** Tienes **una sola suscripción de Azure** para demos y quieres probar el modo `ailz-integrated` completo. Normalmente, la AI Landing Zone ya existe como infraestructura compartida enterprise. Aquí crearemos una **versión mínima ("Mini-AILZ")** en la misma suscripción para simular ese escenario. + +--- + +## 2. ¿Qué es el Modo AILZ-Integrated? + +En un entorno enterprise real, el **AI Landing Zone** es una infraestructura de red pre-provisionada por el equipo de plataforma que incluye: + +- **Virtual Network (VNet)** con subredes dedicadas +- **Private DNS Zones** para resolución de nombres internos +- **Network Security Groups (NSGs)** y políticas de seguridad +- **JumpBox VM** para acceso administrativo +- **Shared Log Analytics** y **Application Insights** centralizados +- **Azure Firewall** o NVA para control de tráfico (enterprise completo) + +ContentFlow en modo `ailz-integrated` **no crea red propia**. En su lugar: + +1. **Reutiliza** la VNet y subredes existentes del AILZ +2. **Crea Private Endpoints** en la subred dedicada (`pe-subnet`) +3. **Registra registros DNS** en las Private DNS Zones existentes +4. **Configura todos los recursos** con `publicNetworkAccess: Disabled` +5. **Integra Container Apps Environment** con la VNet (modo interno) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ AI Landing Zone (VNet) │ +│ │ +│ ┌──────────────────────┐ ┌────────────────────────────────────┐ │ +│ │ pe-subnet (/27+) │ │ aca-env-subnet (/23) │ │ +│ │ │ │ │ │ +│ │ ● Storage PE (blob) │ │ ┌──────────────────────────────┐ │ │ +│ │ ● Storage PE (queue)│ │ │ Container Apps Environment │ │ │ +│ │ ● Cosmos DB PE │ │ │ (Internal Load Balancer) │ │ │ +│ │ ● App Config PE │ │ │ │ │ │ +│ │ ● ACR PE (Premium) │ │ │ ┌─────┐ ┌──────┐ ┌─────┐ │ │ │ +│ │ ● AI Foundry PE │ │ │ │ API │ │Worker│ │ Web │ │ │ │ +│ │ │ │ │ └─────┘ └──────┘ └─────┘ │ │ │ +│ └──────────────────────┘ │ └──────────────────────────────┘ │ │ +│ └────────────────────────────────────┘ │ +│ │ +│ ┌──────────────┐ ┌──────────────────────────────────────────────┐ │ +│ │ jumpbox-vm │ │ Private DNS Zones (6 requeridas) │ │ +│ │ (acceso) │ │ ● privatelink.blob.core.windows.net │ │ +│ └──────────────┘ │ ● privatelink.documents.azure.com │ │ +│ │ ● privatelink.azconfig.io │ │ +│ │ ● privatelink.azurecr.io │ │ +│ │ ● privatelink.cognitiveservices.azure.com │ │ +│ │ ● privatelink.azurecontainerapps.io │ │ +│ └──────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 3. Arquitectura de Red: Basic vs AILZ + +### Modo Basic (Público) + +``` +Internet ──→ Container Apps (API/Web) ──→ Storage Account (público) + ──→ Cosmos DB (público) + ──→ App Config (público) + ──→ ACR Standard (público) + ──→ AI Foundry (público) +``` + +- Todos los recursos tienen endpoints públicos +- Accesibles desde cualquier IP +- Network ACLs: `defaultAction: Allow` + +### Modo AILZ-Integrated (Privado) + +``` +JumpBox/VPN ──→ VNet ──→ Container Apps (interno) ──→ Storage PE (privado) + ──→ Cosmos PE (privado) + ──→ App Config PE (privado) + ──→ ACR PE Premium (privado) + ──→ AI Foundry PE (privado) +``` + +- Todos los recursos: `publicNetworkAccess: Disabled` +- Network ACLs: `defaultAction: Deny`, `bypass: AzureServices` +- Acceso solo vía Private Endpoints dentro de la VNet +- Container Apps: `internal: true`, solo accesible desde VNet + +--- + +## 4. Prerrequisitos Generales + +### Herramientas Requeridas + +```bash +# Verificar instalación +az --version # Azure CLI 2.60+ +azd version # Azure Developer CLI 1.5+ +docker --version # Docker Desktop (para build de containers) +``` + +### Permisos Azure Requeridos + +| Permiso | Escenario | Razón | +|---|---|---| +| `Contributor` | Resource Group de ContentFlow | Crear todos los recursos | +| `Network Contributor` | Resource Group de la VNet | Crear Private Endpoints en subredes | +| `Private DNS Zone Contributor` | Resource Group de DNS Zones | Crear registros A para PEs | +| `User Access Administrator` | Resource Group | Asignar RBAC a Managed Identity | + +> **Nota:** En el escenario Mini-AILZ (suscripción única), si usas un Resource Group separado para la red, necesitas permisos en ambos RGs. + +### Registro de Providers + +```bash +# Asegurar que estos providers estén registrados +az provider register --namespace Microsoft.App +az provider register --namespace Microsoft.ContainerRegistry +az provider register --namespace Microsoft.DocumentDB +az provider register --namespace Microsoft.Network +az provider register --namespace Microsoft.CognitiveServices +az provider register --namespace Microsoft.Storage +az provider register --namespace Microsoft.AppConfiguration +``` + +--- + +## 5. El Problema: Demo con Suscripción Única + +En un entorno enterprise real: + +``` +┌──────────────────────────────┐ ┌──────────────────────────────┐ +│ Suscripción: Platform │ │ Suscripción: Workloads │ +│ │ │ │ +│ RG: rg-ailz-network │ │ RG: rg-contentflow │ +│ ├── VNet │ │ ├── Storage + PE │ +│ ├── Private DNS Zones │ │ ├── Cosmos DB + PE │ +│ ├── NSGs │ │ ├── Container Apps (int) │ +│ ├── Azure Firewall │ │ ├── ACR Premium + PE │ +│ └── JumpBox VM │ │ ├── App Config + PE │ +│ │ │ └── AI Foundry + PE │ +│ (Manejado por Platform Team)│ │ (Manejado por App Team) │ +└──────────────────────────────┘ └──────────────────────────────┘ +``` + +**Tu situación:** Solo tienes **una suscripción**. No existe un AILZ pre-provisionado. Necesitas: + +1. **Simular** la infraestructura de red que normalmente provee el Platform Team +2. **Desplegar** ContentFlow en modo `ailz-integrated` apuntando a esa red +3. **Acceder** a los servicios internos para validar la demo + +--- + +## 6. Solución: AI Landing Zone Mínima ("Mini-AILZ") + +Crearemos la infraestructura mínima necesaria en un **Resource Group separado** dentro de la misma suscripción: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Suscripción Única de Demo │ +│ │ +│ ┌──────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ RG: rg-mini-ailz │ │ RG: rg-contentflow-ailz │ │ +│ │ │ │ │ │ +│ │ ● VNet (10.0.0.0/16) │ │ ● Storage + PE │ │ +│ │ ├── pe-subnet /27 │ │ ● Cosmos DB + PE │ │ +│ │ ├── aca-env-subnet /23 │ │ ● ACR Premium + PE │ │ +│ │ └── jumpbox-subnet /27 │ │ ● App Config + PE │ │ +│ │ │ │ ● Container Apps Env (int) │ │ +│ │ ● 6 Private DNS Zones │ │ ● Container Apps (API/ │ │ +│ │ (vinculadas a la VNet) │ │ Worker/Web) │ │ +│ │ │ │ ● AI Foundry + PE │ │ +│ │ ● JumpBox VM (B2s) │ │ ● App Insights (nuevo) │ │ +│ │ (con Bastion o IP pública │ │ ● Log Analytics (nuevo) │ │ +│ │ temporal para demo) │ │ ● User Assigned Identity │ │ +│ │ │ │ │ │ +│ │ ● Log Analytics (opcional) │ │ │ │ +│ │ ● App Insights (opcional) │ │ │ │ +│ └──────────────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### ¿Por qué un RG separado? + +- **Simula el escenario real**: Red y aplicación en RGs diferentes +- **Limpieza fácil**: Puedes borrar `rg-contentflow-ailz` sin afectar la red +- **Permisos realistas**: Puedes validar que los permisos cross-RG funcionan +- **Reutilizable**: La Mini-AILZ puede servir para otras demos de servicios con Private Endpoints + +--- + +## 7. Paso a Paso: Crear la Mini-AILZ + +### 7.1 Definir Variables + +```bash +# === CONFIGURACIÓN === +# Ajusta estos valores a tu ambiente +SUBSCRIPTION_ID=$(az account show --query id -o tsv) +LOCATION="eastus2" # Tu región preferida +AILZ_RG="rg-mini-ailz" # RG para la infraestructura de red +CF_RG="rg-contentflow-ailz" # RG para ContentFlow (lo crea azd) +VNET_NAME="vnet-ailz-demo" +VNET_PREFIX="10.0.0.0/16" + +# Subredes +PE_SUBNET_NAME="pe-subnet" # Nombre esperado por ContentFlow +PE_SUBNET_PREFIX="10.0.1.0/27" # /27 = 32 IPs (suficiente para ~25 PEs) +ACA_SUBNET_NAME="aca-env-subnet" # Nombre esperado por ContentFlow +ACA_SUBNET_PREFIX="10.0.16.0/23" # /23 = 512 IPs (requerido por Container Apps) +JUMPBOX_SUBNET_NAME="jumpbox-subnet" +JUMPBOX_SUBNET_PREFIX="10.0.2.0/27" # /27 = 32 IPs +``` + +### 7.2 Crear Resource Group de Red + +```bash +az group create \ + --name $AILZ_RG \ + --location $LOCATION \ + --tags purpose=mini-ailz owner=demo +``` + +### 7.3 Crear Virtual Network con Subredes + +```bash +# Crear VNet +az network vnet create \ + --resource-group $AILZ_RG \ + --name $VNET_NAME \ + --address-prefix $VNET_PREFIX \ + --location $LOCATION \ + --tags purpose=mini-ailz + +# Subred para Private Endpoints +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name $PE_SUBNET_NAME \ + --address-prefix $PE_SUBNET_PREFIX + +# Subred para Container Apps Environment +# IMPORTANTE: Container Apps necesita /23 mínimo y la subred debe estar delegada +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name $ACA_SUBNET_NAME \ + --address-prefix $ACA_SUBNET_PREFIX + +# Subred para JumpBox +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name $JUMPBOX_SUBNET_NAME \ + --address-prefix $JUMPBOX_SUBNET_PREFIX +``` + +> **Nota sobre el tamaño de subred para Container Apps:** Container Apps Environment en modo VNet-integrated requiere una subred de al menos `/23` (512 direcciones). Esto es un requisito de la plataforma para manejar la infraestructura interna del entorno. + +### 7.4 Crear las 6 Private DNS Zones Requeridas + +Cada zona debe vincularse (link) a la VNet para que la resolución DNS funcione: + +```bash +# Lista de zonas requeridas por ContentFlow +DNS_ZONES=( + "privatelink.blob.core.windows.net" + "privatelink.documents.azure.com" + "privatelink.azconfig.io" + "privatelink.azurecr.io" + "privatelink.cognitiveservices.azure.com" + "privatelink.azurecontainerapps.io" +) + +# Crear cada zona y vincularla a la VNet +for ZONE in "${DNS_ZONES[@]}"; do + echo "Creando DNS Zone: $ZONE" + + # Crear la zona + az network private-dns zone create \ + --resource-group $AILZ_RG \ + --name "$ZONE" \ + --tags purpose=mini-ailz + + # Vincular la zona a la VNet (CRÍTICO - sin esto la resolución DNS no funciona) + LINK_NAME=$(echo "$ZONE" | sed 's/\./-/g')-link + az network private-dns link vnet create \ + --resource-group $AILZ_RG \ + --zone-name "$ZONE" \ + --name "$LINK_NAME" \ + --virtual-network "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" \ + --registration-enabled false +done + +echo "✓ 6 Private DNS Zones creadas y vinculadas a la VNet" +``` + +### 7.5 (Opcional) Crear Private DNS Zone para Key Vault + +```bash +# Solo si planeas usar Key Vault con Private Endpoint +az network private-dns zone create \ + --resource-group $AILZ_RG \ + --name "privatelink.vaultcore.azure.net" \ + --tags purpose=mini-ailz + +az network private-dns link vnet create \ + --resource-group $AILZ_RG \ + --zone-name "privatelink.vaultcore.azure.net" \ + --name "privatelink-vaultcore-azure-net-link" \ + --virtual-network "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" \ + --registration-enabled false +``` + +### 7.6 Crear JumpBox VM + +La JumpBox es necesaria para acceder a los servicios internos después del despliegue. Tienes **tres opciones**: + +#### Opción A: JumpBox con Azure Bastion (Recomendada para Demo) + +Azure Bastion provee acceso seguro RDP/SSH sin exponer IP pública en la VM. + +```bash +# Crear subred para Azure Bastion (REQUIERE nombre exacto y mínimo /26) +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name "AzureBastionSubnet" \ + --address-prefix "10.0.3.0/26" + +# Crear IP pública para Bastion +az network public-ip create \ + --resource-group $AILZ_RG \ + --name "pip-bastion-demo" \ + --sku Standard \ + --allocation-method Static \ + --location $LOCATION + +# Crear Azure Bastion (Developer SKU - más económico) +az network bastion create \ + --resource-group $AILZ_RG \ + --name "bastion-demo" \ + --public-ip-address "pip-bastion-demo" \ + --vnet-name $VNET_NAME \ + --sku Developer \ + --location $LOCATION + +# Crear la JumpBox VM (sin IP pública - Bastion proporciona acceso) +az vm create \ + --resource-group $AILZ_RG \ + --name "jmp-ailz-demo" \ + --image "MicrosoftWindowsDesktop:windows-11:win11-24h2-pro:latest" \ + --size "Standard_B2ms" \ + --vnet-name $VNET_NAME \ + --subnet $JUMPBOX_SUBNET_NAME \ + --public-ip-address "" \ + --admin-username "azureuser" \ + --admin-password "$(openssl rand -base64 16)A1!" \ + --tags role=jumpbox purpose=mini-ailz \ + --location $LOCATION +``` + +> **Importante:** Anota la contraseña generada. También puedes usar `--admin-password` con un valor específico que controles. + +#### Opción B: JumpBox con IP Pública Temporal (Más Simple) + +```bash +# JumpBox con IP pública (solo para demo, desactivar después) +az vm create \ + --resource-group $AILZ_RG \ + --name "jmp-ailz-demo" \ + --image "MicrosoftWindowsDesktop:windows-11:win11-24h2-pro:latest" \ + --size "Standard_B2ms" \ + --vnet-name $VNET_NAME \ + --subnet $JUMPBOX_SUBNET_NAME \ + --admin-username "azureuser" \ + --admin-password "TuPasswordSeguro123!" \ + --tags role=jumpbox purpose=mini-ailz \ + --nsg-rule RDP \ + --location $LOCATION +``` + +> **Seguridad:** Restringe RDP solo a tu IP: +> ```bash +> MY_IP=$(curl -s https://api.ipify.org) +> az network nsg rule update \ +> --resource-group $AILZ_RG \ +> --nsg-name "jmp-ailz-demoNSG" \ +> --name "rdp" \ +> --source-address-prefixes "$MY_IP/32" +> ``` + +#### Opción C: JumpBox Linux (Más Económica) + +```bash +az vm create \ + --resource-group $AILZ_RG \ + --name "jmp-ailz-demo" \ + --image "Ubuntu2404" \ + --size "Standard_B2s" \ + --vnet-name $VNET_NAME \ + --subnet $JUMPBOX_SUBNET_NAME \ + --admin-username "azureuser" \ + --generate-ssh-keys \ + --tags role=jumpbox purpose=mini-ailz \ + --public-ip-address "" \ + --location $LOCATION +``` + +### 7.7 (Opcional) Crear Observabilidad Compartida + +Si quieres simular el escenario enterprise donde Log Analytics y App Insights son compartidos: + +```bash +# Log Analytics Workspace compartido +az monitor log-analytics workspace create \ + --resource-group $AILZ_RG \ + --workspace-name "log-ailz-shared" \ + --sku PerGB2018 \ + --location $LOCATION \ + --tags purpose=mini-ailz + +# Application Insights compartido +LOG_WS_ID=$(az monitor log-analytics workspace show \ + --resource-group $AILZ_RG \ + --workspace-name "log-ailz-shared" \ + --query id -o tsv) + +az monitor app-insights component create \ + --resource-group $AILZ_RG \ + --app "appi-ailz-shared" \ + --location $LOCATION \ + --workspace "$LOG_WS_ID" \ + --tags purpose=mini-ailz +``` + +> **Nota:** Si no creas estos recursos, ContentFlow en modo `ailz-integrated` **creará sus propios** Log Analytics y App Insights (el código Bicep lo contempla). Solo necesitas crearlos si quieres probar el escenario de observabilidad compartida. + +### 7.8 Verificar la Mini-AILZ + +```bash +echo "=== Verificación de Mini-AILZ ===" + +echo "VNet:" +az network vnet show -g $AILZ_RG -n $VNET_NAME --query "{name:name, address:addressSpace.addressPrefixes[0]}" -o table + +echo "" +echo "Subredes:" +az network vnet subnet list -g $AILZ_RG --vnet-name $VNET_NAME --query "[].{name:name, prefix:addressPrefix}" -o table + +echo "" +echo "Private DNS Zones:" +az network private-dns zone list -g $AILZ_RG --query "[].{name:name, links:numberOfVirtualNetworkLinks}" -o table + +echo "" +echo "JumpBox VM:" +az vm list -g $AILZ_RG --query "[].{name:name, size:hardwareProfile.vmSize, tags:tags.role}" -o table +``` + +Salida esperada: +``` +=== Verificación de Mini-AILZ === +VNet: +Name Address +----------- ---------- +vnet-ailz-demo 10.0.0.0/16 + +Subredes: +Name Prefix +----------- ---------- +pe-subnet 10.0.1.0/27 +aca-env-subnet 10.0.16.0/23 +jumpbox-subnet 10.0.2.0/27 + +Private DNS Zones: +Name Links +--------------------------------------------- ----- +privatelink.blob.core.windows.net 1 +privatelink.documents.azure.com 1 +privatelink.azconfig.io 1 +privatelink.azurecr.io 1 +privatelink.cognitiveservices.azure.com 1 +privatelink.azurecontainerapps.io 1 + +JumpBox VM: +Name Size Tags +----------- ----------- -------- +jmp-ailz-demo Standard_B2ms jumpbox +``` + +--- + +## 8. Paso a Paso: Desplegar ContentFlow en Modo AILZ + +### 8.1 Obtener IDs de Recursos de la Mini-AILZ + +Usa el script incluido en ContentFlow o hazlo manualmente: + +#### Opción A: Script Automático (Recomendada) + +```bash +cd infra/scripts + +# Ejecutar con auto-set (configura variables de azd automáticamente) +./get-ailz-resources.sh --auto-set +# Cuando pregunte por el RG, ingresa: rg-mini-ailz +``` + +El script busca automáticamente: +- VNet y subredes (`pe-subnet`, `aca-env-subnet`) +- Las 6 Private DNS Zones requeridas +- JumpBox VM (busca tag `role=jumpbox` o nombre con `jmp`) +- Log Analytics y App Insights (opcionales) + +Genera un archivo `ailz-resources.env` y con `--auto-set` configura las variables en azd directamente. + +#### Opción B: Recopilación Manual + +```bash +# Obtener VNet Resource ID +VNET_ID=$(az network vnet show -g $AILZ_RG -n $VNET_NAME --query id -o tsv) +echo "VNET_ID: $VNET_ID" + +# Obtener IDs de Private DNS Zones +BLOB_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.blob.core.windows.net" --query id -o tsv) +COSMOS_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.documents.azure.com" --query id -o tsv) +APPCONFIG_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azconfig.io" --query id -o tsv) +ACR_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azurecr.io" --query id -o tsv) +COGNITIVE_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.cognitiveservices.azure.com" --query id -o tsv) +ACA_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azurecontainerapps.io" --query id -o tsv) + +# Opcionales +LOG_WS_ID=$(az monitor log-analytics workspace show -g $AILZ_RG -n "log-ailz-shared" --query id -o tsv 2>/dev/null || echo "") +APPI_ID=$(az monitor app-insights component show -g $AILZ_RG --app "appi-ailz-shared" --query id -o tsv 2>/dev/null || echo "") +``` + +### 8.2 Inicializar Entorno de azd + +```bash +# Ir al raíz del repositorio +cd /ruta/a/contentflow + +# Inicializar nuevo entorno azd +azd init -e contentflow-ailz-demo + +# Configurar ubicación y suscripción +azd env set AZURE_LOCATION "$LOCATION" +azd env set AZURE_AI_FOUNDRY_LOCATION "$LOCATION" +``` + +### 8.3 Configurar Variables de Modo AILZ + +```bash +# === MODO DE DESPLIEGUE === +azd env set DEPLOYMENT_MODE "ailz-integrated" + +# === RED === +azd env set EXISTING_VNET_RESOURCE_ID "$VNET_ID" +azd env set PRIVATE_ENDPOINT_SUBNET_NAME "pe-subnet" +azd env set CONTAINER_APPS_SUBNET_NAME "aca-env-subnet" + +# === PRIVATE DNS ZONES (6 requeridas) === +azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID "$BLOB_DNS_ID" +azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID "$COSMOS_DNS_ID" +azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID "$APPCONFIG_DNS_ID" +azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID "$ACR_DNS_ID" +azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID "$COGNITIVE_DNS_ID" +azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$ACA_DNS_ID" + +# === OPCIONALES: OBSERVABILIDAD COMPARTIDA === +# Solo si creaste Log Analytics y App Insights compartidos en el paso 7.7 +if [ ! -z "$LOG_WS_ID" ]; then + azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$LOG_WS_ID" +fi +if [ ! -z "$APPI_ID" ]; then + azd env set EXISTING_APP_INSIGHTS_ID "$APPI_ID" +fi + +# === Principal ID (tu usuario para RBAC) === +PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv) +azd env set AZURE_PRINCIPAL_ID "$PRINCIPAL_ID" +``` + +### 8.4 Verificar Configuración + +```bash +# Listar todas las variables del entorno azd +azd env get-values | grep -E "DEPLOYMENT_MODE|EXISTING_|PRIVATE_|CONTAINER_APPS_SUBNET|AZURE_LOCATION" +``` + +Salida esperada: +``` +DEPLOYMENT_MODE="ailz-integrated" +EXISTING_VNET_RESOURCE_ID="/subscriptions/.../virtualNetworks/vnet-ailz-demo" +PRIVATE_ENDPOINT_SUBNET_NAME="pe-subnet" +CONTAINER_APPS_SUBNET_NAME="aca-env-subnet" +EXISTING_BLOB_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.blob.core.windows.net" +EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.documents.azure.com" +EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azconfig.io" +EXISTING_ACR_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azurecr.io" +EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.cognitiveservices.azure.com" +EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azurecontainerapps.io" +AZURE_LOCATION="eastus2" +``` + +### 8.5 Desplegar + +```bash +# Despliegue completo (provision + build + deploy) +azd up +``` + +**¿Qué sucede durante `azd up`?** + +1. **Pre-provision hook**: Valida que Azure CLI, azd y Docker estén instalados +2. **Bicep Provisioning**: + - Valida parámetros AILZ (all 6 DNS zones + VNet + subredes) + - Crea User Assigned Identity + - Crea Storage Account con PE en `pe-subnet` → registra en `privatelink.blob.core.windows.net` + - Crea Cosmos DB con PE en `pe-subnet` → registra en `privatelink.documents.azure.com` + - Crea App Configuration con PE en `pe-subnet` → registra en `privatelink.azconfig.io` + - Crea Container Registry **Premium** con PE en `pe-subnet` → registra en `privatelink.azurecr.io` + - Crea Container Apps Environment **interno** en `aca-env-subnet` + - Crea Container Apps (API, Worker, Web) con ingress **interno** + - Crea AI Foundry Hub & Project + - Si no proporcionaste Log Analytics/App Insights existentes: crea nuevos + - Configura RBAC para la Managed Identity +3. **Container Build**: Construye imágenes Docker para API, Worker, Web +4. **Push to ACR**: Sube imágenes a Container Registry (vía PE si estás en la VNet, o vía Azure CLI si públicamente) +5. **Deploy to Container Apps**: Despliega las apps (internamente accesibles) +6. **Post-deploy hook**: Muestra endpoints (internos) + +> **⚠️ IMPORTANTE sobre el push a ACR:** En modo AILZ, el ACR tiene `publicNetworkAccess: Disabled`. Si ejecutas `azd up` desde tu máquina local (fuera de la VNet), el push de imágenes **podría fallar**. Ver sección de Troubleshooting para soluciones. + +### 8.6 Verificar Despliegue + +```bash +# Verificar que los Private Endpoints se crearon correctamente +az network private-endpoint list \ + --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) \ + --query "[].{name:name, status:privateLinkServiceConnections[0].privateLinkServiceConnectionState.status}" \ + -o table +``` + +Salida esperada: +``` +Name Status +----------------------- --------- +stXXXXX-blob-pe Approved +stXXXXX-queue-pe Approved +cosmos-XXXXX-pe Approved +appcs-XXXXX-pe Approved +crXXXXX-pe Approved +``` + +--- + +## 9. Acceso y Pruebas de Conectividad Privada + +### 9.1 Acceder vía JumpBox + +Dado que todos los servicios son internos, necesitas estar dentro de la VNet: + +```bash +# Conectar a la JumpBox vía Bastion (Portal Azure) +# O vía Azure CLI: +az network bastion ssh \ + --resource-group $AILZ_RG \ + --name "bastion-demo" \ + --target-resource-id $(az vm show -g $AILZ_RG -n "jmp-ailz-demo" --query id -o tsv) \ + --auth-type password \ + --username azureuser +``` + +### 9.2 Conseguir URLs Internas + +Las URLs de Container Apps en modo interno tienen el formato: +``` +https://...azurecontainerapps.io +``` + +Pero solo son resolvibles dentro de la VNet (gracias a la Private DNS Zone `privatelink.azurecontainerapps.io`). + +```bash +# Obtener URLs internas desde azd +azd env get-values | grep "SERVICE_.*_URI" + +# O directamente: +API_URL=$(az containerapp show -g $(azd env get-value AZURE_RESOURCE_GROUP) -n $(azd env get-value API_CONTAINER_APP_NAME) --query "properties.configuration.ingress.fqdn" -o tsv) +WEB_URL=$(az containerapp show -g $(azd env get-value AZURE_RESOURCE_GROUP) -n $(azd env get-value WEB_CONTAINER_APP_NAME) --query "properties.configuration.ingress.fqdn" -o tsv) + +echo "API: https://$API_URL" +echo "Web: https://$WEB_URL" +``` + +### 9.3 Probar desde la JumpBox + +Desde la JumpBox (Windows): +```powershell +# Verificar resolución DNS (debe resolver a IP privada 10.x.x.x) +nslookup + +# Probar API health check +Invoke-WebRequest -Uri "https:///health" -UseBasicParsing + +# Abrir navegador para la Web UI +Start-Process "https://" +``` + +Desde la JumpBox (Linux): +```bash +# Verificar resolución DNS +nslookup +# Debe retornar IP privada (10.x.x.x), NO una IP pública + +# Probar API +curl -k https:///health + +# Probar que los Private Endpoints resuelven correctamente +nslookup .blob.core.windows.net +# Debe retornar privatelink.blob.core.windows.net → 10.x.x.x + +nslookup .documents.azure.com +# Debe retornar privatelink.documents.azure.com → 10.x.x.x +``` + +### 9.4 Probar Resolución DNS de Cada Servicio + +```bash +# Desde la JumpBox, verificar que todos los PEs resuelven a IPs privadas +SERVICES=( + ".blob.core.windows.net" + ".queue.core.windows.net" + ".documents.azure.com" + ".azconfig.io" + ".azurecr.io" +) + +for SVC in "${SERVICES[@]}"; do + echo "=== $SVC ===" + nslookup $SVC + echo "" +done +``` + +--- + +## 10. Mapa Completo de Parámetros + +### Variables de Entorno azd → Parámetros Bicep + +| Variable azd | Parámetro Bicep | Requerido en AILZ | Default | Descripción | +|---|---|---|---|---| +| `DEPLOYMENT_MODE` | `deploymentMode` | ✅ | — | Debe ser `ailz-integrated` | +| `AZURE_ENV_NAME` | `environmentName` | ✅ | — | Nombre del entorno | +| `AZURE_LOCATION` | `location` | ✅ | — | Región Azure | +| `AZURE_AI_FOUNDRY_LOCATION` | `foundryLocation` | ✅ | — | Región para AI Foundry | +| `AZURE_PRINCIPAL_ID` | `principalId` | ⬜ | `""` | Tu Object ID para RBAC | +| `EXISTING_VNET_RESOURCE_ID` | `existingVnetResourceId` | ✅ | `""` | Resource ID completo de la VNet | +| `PRIVATE_ENDPOINT_SUBNET_NAME` | `privateEndpointSubnetName` | ✅ | `pe-subnet` | Nombre de la subred para PEs | +| `CONTAINER_APPS_SUBNET_NAME` | `containerAppsSubnetName` | ✅ | `aca-env-subnet` | Nombre de la subred para CAE | +| `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | `existingCognitiveServicesPrivateDnsZoneId` | ✅ | `""` | DNS Zone para AI Services | +| `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | `existingBlobPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Blob Storage | +| `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | `existingCosmosPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Cosmos DB | +| `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | `existingAppConfigPrivateDnsZoneId` | ✅ | `""` | DNS Zone para App Config | +| `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | `existingAcrPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Container Registry | +| `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | `existingContainerAppsEnvPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Container Apps | +| `EXISTING_KEY_VAULT_PRIVATE_DNS_ZONE_ID` | `existingKeyVaultPrivateDnsZoneId` | ⬜ | `""` | DNS Zone para Key Vault | +| `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | `existingLogAnalyticsWorkspaceId` | ⬜ | `""` | Log Analytics compartido | +| `EXISTING_APP_INSIGHTS_ID` | `existingAppInsightsId` | ⬜ | `""` | App Insights compartido | + +### Formato de Resource IDs + +``` +VNet: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{name} +DNS Zone: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/{zone-name} +Log Analyt: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{name} +App Insigh: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Insights/components/{name} +``` + +--- + +## 11. Cambios por Recurso en Modo AILZ + +### Comparativa Detallada + +| Recurso | Basic | AILZ-Integrated | +|---|---|---| +| **Storage Account** | SKU: Standard_LRS, Public | SKU: Standard_LRS, **PE (blob+queue)**, `Deny` ACL | +| **Cosmos DB** | Serverless, Public | Serverless, **PE**, `Disabled` public access | +| **App Configuration** | Standard, Public | Standard, **PE**, `Disabled` public access | +| **Container Registry** | **Standard** | **Premium** (requerido para PE), **PE** | +| **Container Apps Env** | Public, External LB | **VNet-integrated**, **Internal LB**, `aca-env-subnet` | +| **Container Apps (API)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | +| **Container Apps (Worker)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | +| **Container Apps (Web)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | +| **AI Foundry** | Public | **PE** para Cognitive Services | +| **Log Analytics** | Creado nuevo (basic) | Usa existente O crea nuevo | +| **App Insights** | Creado nuevo (basic) | Usa existente O crea nuevo | +| **Managed Identity** | Sin cambios | Sin cambios | + +### Nomenclatura de Private Endpoints + +ContentFlow usa una convención consistente: + +``` +{resourceName}-{service}-pe → Nombre del Private Endpoint +{resourceName}-{service}-plsc → Private Link Service Connection +{service}-dns-zone-group → DNS Zone Group +{service}-config → DNS Zone Group Config +``` + +Ejemplos: +- `st4a7b2c-blob-pe` / `st4a7b2c-blob-plsc` +- `st4a7b2c-queue-pe` / `st4a7b2c-queue-plsc` +- `cosmos-4a7b2c-pe` / `cosmos-4a7b2c-cosmos-plsc` +- `appcs-4a7b2c-pe` / `appcs-4a7b2c-app-config-plsc` +- `cr4a7b2c-pe` / `cr4a7b2c-acr-plsc` + +--- + +## 12. Troubleshooting + +### Problema 1: Error "ACR push failed" durante `azd up` + +**Causa:** ACR tiene `publicNetworkAccess: Disabled`. Tu máquina local no puede hacer push de imágenes. + +**Soluciones:** + +**Solución A - Temporalmente habilitar acceso público al ACR:** +```bash +# Antes de azd deploy +ACR_NAME=$(azd env get-value AZURE_CONTAINER_REGISTRY_NAME) +CF_RG=$(azd env get-value AZURE_RESOURCE_GROUP) + +# Habilitar temporalmente +az acr update -n $ACR_NAME -g $CF_RG --public-network-enabled true + +# Hacer el deploy +azd deploy + +# Volver a deshabilitar +az acr update -n $ACR_NAME -g $CF_RG --public-network-enabled false +``` + +**Solución B - Build y push desde la JumpBox:** +```bash +# Ejecutar azd up desde la JumpBox (dentro de la VNet) +# Requiere instalar azd, az cli, docker en la JumpBox +``` + +**Solución C - Usar ACR Tasks (build en la nube):** +```bash +# ACR Tasks puede hacer build sin necesidad de Docker local +az acr build --registry $ACR_NAME -t contentflow-api:latest ./contentflow-api/ +az acr build --registry $ACR_NAME -t contentflow-worker:latest ./contentflow-worker/ +az acr build --registry $ACR_NAME -t contentflow-web:latest ./contentflow-web/ +``` + +### Problema 2: Error "VNet subnet not found" + +**Causa:** Los nombres de subred no coinciden con los esperados. + +**Solución:** +```bash +# Verificar nombres exactos +az network vnet subnet list -g $AILZ_RG --vnet-name $VNET_NAME --query "[].name" -o tsv + +# Si los nombres son diferentes, ajustar: +azd env set PRIVATE_ENDPOINT_SUBNET_NAME "tu-nombre-de-pe-subnet" +azd env set CONTAINER_APPS_SUBNET_NAME "tu-nombre-de-aca-subnet" +``` + +### Problema 3: Error de validación "fail('existingXXX is required')" + +**Causa:** Falta algún parámetro requerido para modo `ailz-integrated`. + +**Solución:** Ejecutar el script de verificación: +```bash +# Verificar todas las variables están configuradas +REQUIRED_VARS=( + "DEPLOYMENT_MODE" + "EXISTING_VNET_RESOURCE_ID" + "EXISTING_BLOB_PRIVATE_DNS_ZONE_ID" + "EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID" + "EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID" + "EXISTING_ACR_PRIVATE_DNS_ZONE_ID" + "EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID" + "EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID" +) + +for VAR in "${REQUIRED_VARS[@]}"; do + VALUE=$(azd env get-value $VAR 2>/dev/null) + if [ -z "$VALUE" ]; then + echo "❌ FALTA: $VAR" + else + echo "✓ $VAR configurada" + fi +done +``` + +### Problema 4: DNS no resuelve a IP privada desde JumpBox + +**Causa:** La Private DNS Zone no está vinculada a la VNet, o el DNS Zone Group no se creó. + +**Solución:** +```bash +# Verificar que cada zona tiene un VNet link +for ZONE in "privatelink.blob.core.windows.net" "privatelink.documents.azure.com" "privatelink.azconfig.io" "privatelink.azurecr.io" "privatelink.cognitiveservices.azure.com" "privatelink.azurecontainerapps.io"; do + echo "=== $ZONE ===" + az network private-dns link vnet list --zone-name $ZONE -g $AILZ_RG --query "[].{name:name, state:virtualNetworkLinkState}" -o table +done + +# Si falta un link: +az network private-dns link vnet create \ + --resource-group $AILZ_RG \ + --zone-name "privatelink.blob.core.windows.net" \ + --name "blob-vnet-link" \ + --virtual-network "$VNET_ID" \ + --registration-enabled false +``` + +### Problema 5: Container Apps no inician (health check failed) + +**Causa:** Las Container Apps no pueden alcanzar los servicios backend (Storage, Cosmos) porque los PEs no están configurados correctamente o los DNS Zone Groups no registraron los records A. + +**Solución:** +```bash +# Verificar logs de Container Apps +az containerapp logs show \ + -n $(azd env get-value API_CONTAINER_APP_NAME) \ + -g $(azd env get-value AZURE_RESOURCE_GROUP) \ + --type system + +# Verificar que los A records existen en las DNS zones +az network private-dns record-set a list -g $AILZ_RG -z "privatelink.blob.core.windows.net" -o table +az network private-dns record-set a list -g $AILZ_RG -z "privatelink.documents.azure.com" -o table +``` + +### Problema 6: Permisos insuficientes para crear PE en subnet de otro RG + +**Causa:** Tu usuario no tiene `Network Contributor` en el RG de la VNet. + +**Solución:** +```bash +# Asignar permisos en el RG de red +USER_OBJ_ID=$(az ad signed-in-user show --query id -o tsv) + +az role assignment create \ + --assignee $USER_OBJ_ID \ + --role "Network Contributor" \ + --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG" + +az role assignment create \ + --assignee $USER_OBJ_ID \ + --role "Private DNS Zone Contributor" \ + --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG" +``` + +### Problema 7: Error "Container Apps Environment subnet too small" + +**Causa:** La subred `aca-env-subnet` es menor a `/23`. + +**Solución:** Recrear la subred con el tamaño correcto (requiere borrar la existente primero si no tiene recursos): +```bash +az network vnet subnet delete -g $AILZ_RG --vnet-name $VNET_NAME -n $ACA_SUBNET_NAME +az network vnet subnet create \ + --resource-group $AILZ_RG \ + --vnet-name $VNET_NAME \ + --name $ACA_SUBNET_NAME \ + --address-prefix "10.0.16.0/23" +``` + +--- + +## 13. Costos Estimados de la Mini-AILZ + +### Recursos de Red (RG: rg-mini-ailz) + +| Recurso | SKU/Tier | Costo Estimado (USD/mes) | +|---|---|---| +| VNet + Subredes | Gratuito | $0 | +| Private DNS Zones (6) | $0.50/zona | ~$3 | +| Azure Bastion | Developer SKU | ~$5.50 | +| JumpBox VM (B2ms, Windows) | Standard_B2ms | ~$60 (24/7) | +| JumpBox VM (B2s, Linux) | Standard_B2s | ~$15 (24/7) | + +**Tip para reducir costos de demo:** +```bash +# Apagar la JumpBox cuando no la uses +az vm deallocate -g $AILZ_RG -n "jmp-ailz-demo" + +# Encenderla cuando la necesites +az vm start -g $AILZ_RG -n "jmp-ailz-demo" +``` + +### Recursos ContentFlow Adicionales en AILZ vs Basic + +| Recurso | Cambio en AILZ | Impacto en Costo | +|---|---|---| +| Container Registry | Standard → **Premium** | +~$45/mes | +| Private Endpoints (5-6) | Cada PE tiene costo | +~$5/mes total | +| Procesamiento de PE (datos) | Por GB procesado | Mínimo en demo | + +### Costo Total Estimado de Demo (Mini-AILZ + ContentFlow) + +| Componente | Costo Estimado | +|---|---| +| Mini-AILZ (red + JumpBox Linux + Bastion) | ~$24/mes | +| ContentFlow AILZ (Premium ACR, PEs) delta vs Basic | ~$50/mes | +| ContentFlow base (Storage, Cosmos, Container Apps, AI) | ~$30-80/mes | +| **Total estimado** | **~$100-150/mes** | + +> **Nota:** Los costos de AI Foundry (GPT-4.1) son por uso (tokens). El estimado base asume uso mínimo. Apaga la JumpBox cuando no la uses para reducir costos. + +--- + +## 14. Limpieza de Recursos + +### Eliminar ContentFlow + +```bash +# Eliminar todos los recursos de ContentFlow +azd down --force --purge +``` + +> `--purge` elimina los recursos en soft-delete (Cosmos DB, Key Vault, AI Services). + +### Eliminar la Mini-AILZ + +```bash +# Eliminar todo el Resource Group de red +az group delete --name $AILZ_RG --yes --no-wait +``` + +### Limpieza Selectiva (Mantener la Red) + +Si quieres mantener la Mini-AILZ para futuras demos: +```bash +# Solo eliminar ContentFlow +azd down --force --purge + +# Apagar JumpBox para ahorrar +az vm deallocate -g $AILZ_RG -n "jmp-ailz-demo" +``` + +--- + +## Apéndice A: Script Completo de Creación de Mini-AILZ + +Para conveniencia, aquí está el script completo que crea toda la Mini-AILZ de un solo paso: + +```bash +#!/bin/bash +# create-mini-ailz.sh - Crea una AI Landing Zone mínima para demo de ContentFlow +set -e + +# === CONFIGURACIÓN (AJUSTAR SEGÚN TU AMBIENTE) === +SUBSCRIPTION_ID=$(az account show --query id -o tsv) +LOCATION="eastus2" +AILZ_RG="rg-mini-ailz" +VNET_NAME="vnet-ailz-demo" +VNET_PREFIX="10.0.0.0/16" +PE_SUBNET_PREFIX="10.0.1.0/27" +ACA_SUBNET_PREFIX="10.0.16.0/23" +JUMPBOX_SUBNET_PREFIX="10.0.2.0/27" +BASTION_SUBNET_PREFIX="10.0.3.0/26" + +echo "=== Creando Mini-AILZ para ContentFlow Demo ===" +echo "Suscripción: $SUBSCRIPTION_ID" +echo "Ubicación: $LOCATION" +echo "Resource Group: $AILZ_RG" +echo "" + +# 1. Resource Group +echo "[1/6] Creando Resource Group..." +az group create -n $AILZ_RG -l $LOCATION --tags purpose=mini-ailz -o none + +# 2. VNet + Subredes +echo "[2/6] Creando VNet y subredes..." +az network vnet create -g $AILZ_RG -n $VNET_NAME --address-prefix $VNET_PREFIX -l $LOCATION -o none +az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "pe-subnet" --address-prefix $PE_SUBNET_PREFIX -o none +az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "aca-env-subnet" --address-prefix $ACA_SUBNET_PREFIX -o none +az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "jumpbox-subnet" --address-prefix $JUMPBOX_SUBNET_PREFIX -o none +az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "AzureBastionSubnet" --address-prefix $BASTION_SUBNET_PREFIX -o none +echo " ✓ VNet con 4 subredes creada" + +# 3. Private DNS Zones +echo "[3/6] Creando Private DNS Zones..." +VNET_ID="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" +DNS_ZONES=( + "privatelink.blob.core.windows.net" + "privatelink.documents.azure.com" + "privatelink.azconfig.io" + "privatelink.azurecr.io" + "privatelink.cognitiveservices.azure.com" + "privatelink.azurecontainerapps.io" +) +for ZONE in "${DNS_ZONES[@]}"; do + az network private-dns zone create -g $AILZ_RG -n "$ZONE" -o none + LINK_NAME=$(echo "$ZONE" | sed 's/\./-/g')-link + az network private-dns link vnet create -g $AILZ_RG -z "$ZONE" -n "$LINK_NAME" -v "$VNET_ID" -e false -o none +done +echo " ✓ 6 Private DNS Zones creadas y vinculadas" + +# 4. Azure Bastion +echo "[4/6] Creando Azure Bastion..." +az network public-ip create -g $AILZ_RG -n "pip-bastion-demo" --sku Standard --allocation-method Static -l $LOCATION -o none +az network bastion create -g $AILZ_RG -n "bastion-demo" --public-ip-address "pip-bastion-demo" --vnet-name $VNET_NAME --sku Developer -l $LOCATION -o none +echo " ✓ Azure Bastion (Developer SKU) creado" + +# 5. JumpBox VM (Linux para costo mínimo) +echo "[5/6] Creando JumpBox VM..." +az vm create -g $AILZ_RG -n "jmp-ailz-demo" \ + --image "Ubuntu2404" --size "Standard_B2s" \ + --vnet-name $VNET_NAME --subnet "jumpbox-subnet" \ + --admin-username "azureuser" --generate-ssh-keys \ + --tags role=jumpbox purpose=mini-ailz \ + --public-ip-address "" -l $LOCATION -o none +echo " ✓ JumpBox Linux creada" + +# 6. Resumen +echo "" +echo "[6/6] Verificación..." +echo "" +echo "=== Mini-AILZ Creada Exitosamente ===" +echo "" +echo "Resource Group: $AILZ_RG" +echo "VNet: $VNET_NAME ($VNET_PREFIX)" +echo "Subredes:" +echo " - pe-subnet: $PE_SUBNET_PREFIX" +echo " - aca-env-subnet: $ACA_SUBNET_PREFIX" +echo " - jumpbox-subnet: $JUMPBOX_SUBNET_PREFIX" +echo " - AzureBastionSubnet: $BASTION_SUBNET_PREFIX" +echo "" +echo "Private DNS Zones: 6 zonas creadas y vinculadas" +echo "JumpBox: jmp-ailz-demo (acceso vía Bastion)" +echo "" +echo "=== Siguiente paso ===" +echo "Ejecuta el script de descubrimiento de ContentFlow:" +echo " cd infra/scripts && ./get-ailz-resources.sh --auto-set" +echo "Luego despliega:" +echo " azd env set DEPLOYMENT_MODE ailz-integrated" +echo " azd up" +``` + +--- + +## Apéndice B: Diagrama de Flujo de Despliegue + +``` +┌────────────────────────┐ +│ 1. Preparar Mini-AILZ │ +│ (create-mini-ailz.sh) │ +└────────┬───────────────┘ + │ + ▼ +┌────────────────────────┐ +│ 2. Obtener IDs │ +│ (get-ailz-resources │ +│ --auto-set) │ +└────────┬───────────────┘ + │ + ▼ +┌────────────────────────┐ +│ 3. Configurar azd │ +│ DEPLOYMENT_MODE= │ +│ ailz-integrated │ +└────────┬───────────────┘ + │ + ▼ +┌────────────────────────┐ +│ 4. azd up │ +│ (provision + deploy) │ +└────────┬───────────────┘ + │ + ▼ +┌────────────────────────┐ ┌──────────────────────┐ +│ 5. Conectar a │────▶│ 6. Probar servicios │ +│ JumpBox vía Bastion │ │ internos (API, Web) │ +└────────────────────────┘ └──────────────────────┘ +``` + +--- + +> **Última actualización:** Febrero 2026 +> **Aplica a:** ContentFlow con plantillas Bicep para despliegue `basic` y `ailz-integrated` diff --git a/Analysis/05-mini-ailz.md b/Analysis/05-mini-ailz.md new file mode 100644 index 0000000..f7e6714 --- /dev/null +++ b/Analysis/05-mini-ailz.md @@ -0,0 +1,1002 @@ +# Mini-AILZ con Terraform AVM Pattern Module para ContentFlow + +> **Documento técnico operativo** — Despliegue de una AI Landing Zone mínima usando el módulo oficial Terraform AVM `Azure/avm-ptn-aiml-landing-zone`, optimizada para el costo mínimo necesario para ContentFlow en modo `ailz-integrated`. + +--- + +## Tabla de Contenidos + +1. [Objetivo y Contexto](#1-objetivo-y-contexto) +2. [¿Por qué el Pattern Module en lugar de Azure CLI Manual?](#2-por-qué-el-pattern-module-en-lugar-de-azure-cli-manual) +3. [Análisis del Módulo: Todos los Recursos vs Lo Mínimo](#3-análisis-del-módulo-todos-los-recursos-vs-lo-mínimo) +4. [Mapeo: Requisitos ContentFlow → Parámetros del Módulo](#4-mapeo-requisitos-contentflow--parámetros-del-módulo) +5. [Configuración Terraform Mínima Completa](#5-configuración-terraform-mínima-completa) +6. [Conexión de Outputs con ContentFlow azd](#6-conexión-de-outputs-con-contentflow-azd) +7. [Fix Conocido: storage_use_azuread](#7-fix-conocido-storage_use_azuread) +8. [Paso a Paso: Despliegue](#8-paso-a-paso-despliegue) +9. [Comparación de Costo: Full vs Mini](#9-comparación-de-costo-full-vs-mini) +10. [Diagrama de Arquitectura Resultante](#10-diagrama-de-arquitectura-resultante) +11. [Notas de Operación y Troubleshooting](#11-notas-de-operación-y-troubleshooting) +12. [Limpieza de Recursos](#12-limpieza-de-recursos) + +--- + +## 1. Objetivo y Contexto + +### Situación + +ContentFlow soporta despliegue en modo `ailz-integrated`, el cual requiere infraestructura de red pre-existente (VNet, subredes, Private DNS Zones, Container Apps Environment). En el [documento 04](04-ai_lz_options.md) se describe cómo crear esta infraestructura con comandos Azure CLI manuales. + +### Nuevo Enfoque + +En lugar de scripts manuales, usaremos el **módulo oficial Terraform AVM Pattern** para AI/ML Landing Zones: + +- **Módulo**: [`Azure/avm-ptn-aiml-landing-zone/azurerm`](https://github.com/Azure/terraform-azurerm-avm-ptn-aiml-landing-zone) +- **Versión**: `0.4.0` +- **Registry**: [Terraform Registry](https://registry.terraform.io/modules/Azure/avm-ptn-aiml-landing-zone/azurerm/latest) + +### Principio Clave: Solo lo Mínimo Indispensable + +El módulo completo despliega **más de 25 recursos** incluyendo AI Foundry, App Gateway, Firewall, APIM, Build VM, AI Search, Bing Grounding, etc. ContentFlow **no necesita la mayoría de estos**. Este documento configura **exclusivamente** lo que ContentFlow requiere para funcionar en modo privado. + +--- + +## 2. ¿Por qué el Pattern Module en lugar de Azure CLI Manual? + +| Aspecto | Azure CLI Manual (Doc 04) | Terraform AVM Module (Este Doc) | +|---|---|---| +| **Reproducibilidad** | Scripts bash con variables | Declarativo, idempotente, plan/apply | +| **Estado** | No hay tracking de estado | `terraform.tfstate` con full tracking | +| **Drift detection** | Manual (`az resource show`) | `terraform plan` detecta drift | +| **Limpieza** | `az group delete` (todo o nada) | `terraform destroy` selectivo | +| **Validación** | Post-ejecución manual | `terraform validate` + `plan` pre-apply | +| **Subredes y DNS** | Creación manual una por una | El módulo crea subredes automáticamente | +| **Private DNS Zones** | 6 comandos individuales + links | El módulo las crea y vincula | +| **Container Apps Env** | No incluido en doc 04 | Incluido con VNet integration | +| **Bastion** | Configuración manual compleja | Un flag `deploy = true` | +| **Versionamiento** | Copiar/pegar scripts | Module version pinning | +| **Soporte oficial** | Ninguno | Módulo oficial de Microsoft (AVM) | + +**Ventaja principal**: El módulo maneja internamente la creación de subredes con los nombres, tamaños y delegaciones correctas. Esto elimina errores comunes como subredes con prefijos incorrectos o sin delegación para Container Apps. + +--- + +## 3. Análisis del Módulo: Todos los Recursos vs Lo Mínimo + +### Recursos que el módulo PUEDE desplegar (despliegue completo) + +| Recurso | Default `deploy` | ¿ContentFlow lo necesita? | Acción Mini-AILZ | +|---|---|---|---| +| **VNet + Subredes** | Siempre (required) | **SÍ** | ✅ Mantener | +| **Private DNS Zones** | Siempre | **SÍ** | ✅ Mantener | +| **Log Analytics Workspace** | `true` | **SÍ** (compartido) | ✅ Mantener | +| **NSGs** | Siempre | **SÍ** (seguridad) | ✅ Mantener | +| **Container Apps Environment** | `true` | **SÍ** | ✅ Mantener | +| **Bastion** | `true` | **SÍ** (acceso a red privada) | ✅ Mantener | +| **GenAI Container Registry** | `true` | **Parcial** (ContentFlow crea el suyo) | ⚠️ Mantener* | +| **GenAI Key Vault** | Siempre | **Parcial** | ⚠️ Mantener* | +| **GenAI Storage Account** | `true` | **No directamente** | ⚠️ Mantener* | +| **GenAI App Configuration** | `true` | **No directamente** | ⚠️ Mantener* | +| **GenAI Cosmos DB** | `true` | **No directamente** | ⚠️ Mantener* | +| **AI Foundry Hub + BYOR** | `{}` (vacío = no crea) | **No** | ❌ Omitir | +| **AI Model Deployments** | `{}` (vacío = no crea) | **No** | ❌ Omitir | +| **AI Projects** | `{}` (vacío = no crea) | **No** | ❌ Omitir | +| **App Gateway** | `null` (no crea) | **No** | ❌ Omitir | +| **Azure Firewall** | `true` | **No** (demo) | ❌ Deshabilitar | +| **Build VM / Jump VM** | `true` | **No** (usamos Bastion) | ❌ Deshabilitar | +| **AI Search (BYOR)** | `{}` (vacío = no crea) | **No** | ❌ Omitir | +| **AI Search (KS)** | `true` | **No** | ❌ Deshabilitar | +| **Bing Grounding** | `true` | **No** | ❌ Deshabilitar | +| **APIM** | `true` | **No** | ❌ Deshabilitar | +| **WAF Policy** | Solo con App GW | **No** | ❌ Omitir | + +> **\*** Los recursos GenAI (Container Registry, Key Vault, Storage, App Config, Cosmos) se crean por defecto como parte de la plataforma GenAI del módulo. ContentFlow crea sus propios recursos de datos (Cosmos, Storage, App Config) durante `azd up`, pero el módulo necesita el Container Registry y Key Vault internamente. Para minimizar costo, podemos aceptar estos con configuración por defecto ligera. + +### ¿Qué NO se puede deshabilitar? + +El módulo **siempre crea** estos recursos independientemente de la configuración: + +1. **Resource Group** (si no existe) +2. **VNet** (required input) +3. **Subredes** (calculadas internamente según recursos habilitados) +4. **Private DNS Zones** (necesarias para private endpoints) +5. **NSGs** (asociados a subredes) +6. **Key Vault** (usado internamente por el módulo para secretos de GenAI) +7. **Role Assignments** (deployment user → Key Vault Admin) + +--- + +## 4. Mapeo: Requisitos ContentFlow → Parámetros del Módulo + +### Lo que ContentFlow Necesita del AILZ + +| Requisito ContentFlow | Parámetro del Módulo | Variable de Entorno `azd` | +|---|---|---| +| VNet Resource ID | `module.ailz.virtual_network.id` | `EXISTING_VNET_RESOURCE_ID` | +| Subred `pe-subnet` | Creada automáticamente por el módulo | `PRIVATE_ENDPOINT_SUBNET_NAME` | +| Subred `aca-env-subnet` | Creada automáticamente por el módulo | `CONTAINER_APPS_SUBNET_NAME` | +| DNS: `privatelink.blob.core.windows.net` | Parte de `private_dns_zones` del módulo | `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.documents.azure.com` | Parte de `private_dns_zones` del módulo | `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.azconfig.io` | Parte de `private_dns_zones` del módulo | `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.azurecr.io` | Parte de `private_dns_zones` del módulo | `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.cognitiveservices.azure.com` | Parte de `private_dns_zones` del módulo | `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | +| DNS: `privatelink.azurecontainerapps.io` | Parte de `private_dns_zones` del módulo | `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | +| Log Analytics Workspace | `module.ailz.log_analytics_workspace_id` | `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | +| Container Apps Environment | Creado por el módulo (VNet-integrated, interno) | El CAE se crea aquí, ContentFlow lo usa | + +### Nota Importante sobre Subredes + +El módulo AVM Pattern calcula y crea las subredes internamente basándose en qué recursos están habilitados. Los nombres de subredes son determinados por el módulo. ContentFlow espera subredes llamadas `pe-subnet` y `aca-env-subnet`. Puedes usar el bloque `vnet_definition.subnets` para configurar override de nombres si es necesario. + +### Nota sobre Container Apps Environment + +El módulo crea un Container Apps Environment integrado con la VNet. Esto es **exactamente lo que ContentFlow necesita**. Sin embargo, ContentFlow normalmente crea su propio CAE durante `azd up`. Para el modo Mini-AILZ con Terraform, tienes dos opciones: + +1. **Usar el CAE del módulo** — Más eficiente; ContentFlow debe configurarse para no crear un CAE propio +2. **Deshabilitar el CAE del módulo** — ContentFlow crea el suyo durante deploy, usando la subred apropiada + +La **opción recomendada es la 1** (usar el CAE del módulo) ya que garantiza la configuración correcta de VNet integration. + +--- + +## 5. Configuración Terraform Mínima Completa + +### Estructura de Archivos + +``` +mini-ailz-contentflow/ +├── terraform.tf # Providers y versiones +├── variables.tf # Variables de entrada +├── main.tf # Data sources y helpers +├── ailz.tf # Módulo AILZ con configuración mínima +├── outputs.tf # Outputs para ContentFlow +└── terraform.tfvars # Valores (opcional, gitignored) +``` + +### `terraform.tf` — Providers + +```hcl +terraform { + required_version = ">= 1.9, < 2.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.116, < 5.0" + } + azapi = { + source = "Azure/azapi" + version = "~> 2.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.5" + } + } +} + +provider "azurerm" { + features { + cognitive_account { + purge_soft_delete_on_destroy = true + } + key_vault { + purge_soft_delete_on_destroy = true + } + resource_group { + prevent_deletion_if_contains_resources = false + } + } + + # CRÍTICO: Sin esto, las Storage Accounts dan error 403 + # Ver sección 7 para detalles + storage_use_azuread = true +} + +provider "azapi" {} +``` + +### `variables.tf` — Variables de Entrada + +```hcl +variable "location" { + type = string + description = "Región de Azure para los recursos" + default = "swedencentral" +} + +variable "resource_group_name" { + type = string + description = "Nombre del Resource Group para la Mini-AILZ" + default = "rg-mini-ailz-contentflow" +} + +variable "name_prefix" { + type = string + description = "Prefijo para nombres de recursos (máx 10 chars, minúsculas alfanuméricas)" + default = "cfailz" + + validation { + condition = length(var.name_prefix) <= 10 && can(regex("^[a-z0-9]+$", var.name_prefix)) + error_message = "name_prefix debe tener máximo 10 caracteres alfanuméricos en minúsculas." + } +} + +variable "enable_telemetry" { + type = bool + description = "Habilitar telemetría del módulo AVM" + default = false +} +``` + +### `main.tf` — Data Sources + +```hcl +data "azurerm_client_config" "current" {} +``` + +### `ailz.tf` — Módulo AILZ Configuración Mínima + +```hcl +# ============================================================================= +# Mini-AILZ para ContentFlow +# ============================================================================= +# Este archivo configura SOLO lo mínimo indispensable del módulo AVM Pattern +# para satisfacer los requisitos de ContentFlow en modo ailz-integrated: +# 1. VNet con subredes (pe-subnet, aca-env-subnet) +# 2. Private DNS Zones vinculadas a la VNet +# 3. Container Apps Environment (VNet-integrated, internal LB) +# 4. Bastion (acceso a la red privada) +# 5. Log Analytics Workspace (monitoreo compartido) +# 6. NSGs (seguridad de subredes) +# 7. Key Vault + Container Registry (requeridos internamente por el módulo) +# +# TODO lo demás está DESHABILITADO para minimizar costo: +# ✗ AI Foundry, Model Deployments, AI Projects +# ✗ App Gateway, WAF Policy +# ✗ Azure Firewall +# ✗ Build VM, Jump VM +# ✗ AI Search (BYOR y KS) +# ✗ Bing Grounding +# ✗ APIM +# ============================================================================= + +module "ailz" { + source = "Azure/avm-ptn-aiml-landing-zone/azurerm" + version = "0.4.0" + + # ── Ubicación y Resource Group ────────────────────────────────────────────── + location = var.location + resource_group_name = var.resource_group_name + + # ── VNet (REQUIRED) ──────────────────────────────────────────────────────── + # ContentFlow necesita: + # - pe-subnet: para Private Endpoints (~32 IPs, /27+) + # - aca-env-subnet: para Container Apps Environment (~512 IPs, /23) + # + # NOTA: El módulo calcula y crea las subredes internamente. + # Usamos 10.0.0.0/16 para tener espacio suficiente. + vnet_definition = { + address_space = ["10.0.0.0/16"] + enable_diagnostic_settings = false + } + + # ── Container Apps Environment ───────────────────────────────────────────── + # ContentFlow despliega API, Worker y Web como Container Apps. + # Este CAE se integra con la VNet en modo interno (solo accesible desde VNet). + container_app_environment_definition = { + internal_load_balancer_enabled = true + zone_redundancy_enabled = false # false = ahorro de costo para demo + enable_diagnostic_settings = false + workload_profile = [ + { + name = "Consumption" + workload_profile_type = "Consumption" + } + ] + } + + # ── Bastion ──────────────────────────────────────────────────────────────── + # Necesario para acceder a la red privada (UI de ContentFlow, debug, etc.) + # SKU "Basic" es más barato que "Standard" (~$140/mes vs ~$350/mes) + bastion_definition = { + deploy = true + sku = "Basic" + zones = [] # Sin zone redundancy para demo + } + + # ── Log Analytics Workspace ──────────────────────────────────────────────── + # Compartido con ContentFlow para monitoreo centralizado. + law_definition = { + deploy = true + } + + # ── AI Foundry ───────────────────────────────────────────────────────────── + # ContentFlow NO necesita AI Foundry del AILZ. + # (ContentFlow crea su propio AI Foundry durante azd up) + # Dejamos el default vacío {} = no crea hub, projects, ni BYOR resources. + ai_foundry_definition = { + create_byor = false # No crear BYOR resources (AI Search, Cosmos, KV, Storage) + ai_foundry = { + enable_diagnostic_settings = false + } + } + + # ── DESHABILITADOS ───────────────────────────────────────────────────────── + + # App Gateway: ContentFlow no usa App Gateway (acceso via Bastion/VPN) + # Default es null = no se despliega + app_gateway_definition = null + + # Azure Firewall: No necesario para demo (ahorra ~$250/mes) + firewall_definition = { + deploy = false + } + + # Build VM: No necesaria (usamos Bastion + local development) + buildvm_definition = { + deploy = false + } + + # Jump VM: No necesaria si tenemos Bastion + # (jumpvm_definition no tiene deploy flag, se controla por buildvm) + + # KS AI Search: ContentFlow no necesita AI Search federado + ks_ai_search_definition = { + deploy = false + } + + # Bing Grounding: ContentFlow no usa Bing + ks_bing_grounding_definition = { + deploy = false + } + + # APIM: ContentFlow no usa API Management + apim_definition = { + deploy = false + publisher_email = "noreply@example.com" + publisher_name = "N/A" + } + + # ── GenAI Platform Resources ─────────────────────────────────────────────── + # Estos recursos se crean por defecto como parte de la plataforma GenAI. + # ContentFlow crea sus propios Cosmos DB, Storage, App Config durante azd up, + # pero el módulo internamente necesita algunos de estos. + # Configuración mínima para reducir costo: + + genai_container_registry_definition = { + zone_redundancy_enabled = false # Ahorro: sin ZRS + enable_diagnostic_settings = false + } + + genai_key_vault_definition = { + # Acceso público habilitado para facilitar deploy desde local + # En producción: false + Private Endpoint + public_network_access_enabled = true + network_acls = { + bypass = "AzureServices" + default_action = "Deny" + } + } + + genai_storage_account_definition = { + account_replication_type = "LRS" # LRS es más barato que GRS (default) + enable_diagnostic_settings = false + } + + genai_app_configuration_definition = { + purge_protection_enabled = false # Facilita destroy para demo + enable_diagnostic_settings = false + } + + genai_cosmosdb_definition = { + analytical_storage_enabled = false # No necesario para demo + automatic_failover_enabled = false # No necesario para single-region demo + enable_diagnostic_settings = false + } + + # ── Flags y otros ────────────────────────────────────────────────────────── + # flag_platform_landing_zone = false → No crea route tables con Firewall + # (Si es true, se asume que hay un Firewall y crea UDRs para enrutar tráfico) + flag_platform_landing_zone = false + + enable_telemetry = var.enable_telemetry + name_prefix = var.name_prefix +} +``` + +### `outputs.tf` — Valores para ContentFlow + +```hcl +# ============================================================================= +# Outputs para configurar ContentFlow azd environment +# ============================================================================= +# Estos valores se usan como variables de entorno para: +# azd env set +# antes de ejecutar: azd up --deployment-mode ailz-integrated +# ============================================================================= + +output "vnet_resource_id" { + description = "VNet Resource ID → EXISTING_VNET_RESOURCE_ID" + value = module.ailz.virtual_network.id +} + +output "log_analytics_workspace_id" { + description = "Log Analytics Workspace ID → EXISTING_LOG_ANALYTICS_WORKSPACE_ID" + value = module.ailz.log_analytics_workspace_id +} + +output "subnets" { + description = "Mapa de subredes creadas (para verificar nombres)" + value = module.ailz.subnets +} + +# ============================================================================= +# NOTA SOBRE PRIVATE DNS ZONES: +# ============================================================================= +# El módulo crea las Private DNS Zones automáticamente, pero actualmente (v0.4.0) +# NO expone sus Resource IDs como outputs directos. +# +# Para obtener los IDs de las DNS Zones, después de `terraform apply`: +# +# az network private-dns zone list \ +# --resource-group \ +# --query "[].{name:name, id:id}" -o table +# +# Alternativamente, puedes usar data sources de Terraform para leerlos: +# ============================================================================= + +# Instrucciones que se imprimen después del apply +output "contentflow_setup_instructions" { + description = "Instrucciones para configurar ContentFlow con esta Mini-AILZ" + value = <<-EOT + + ╔══════════════════════════════════════════════════════════════════╗ + ║ Mini-AILZ desplegada exitosamente para ContentFlow ║ + ╚══════════════════════════════════════════════════════════════════╝ + + Próximos pasos: + + 1. Obtener los Resource IDs de las Private DNS Zones: + + az network private-dns zone list \ + --resource-group ${var.resource_group_name} \ + --query "[].{name:name, id:id}" -o table + + 2. Configurar el environment de ContentFlow: + + cd + azd env set DEPLOYMENT_MODE ailz-integrated + azd env set EXISTING_VNET_RESOURCE_ID ${module.ailz.virtual_network.id} + azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID ${module.ailz.log_analytics_workspace_id} + + # Copiar los IDs de DNS Zones del paso 1: + azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID + azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID + + 3. Desplegar ContentFlow: + + azd up + + EOT +} +``` + +--- + +## 6. Conexión de Outputs con ContentFlow azd + +Después de ejecutar `terraform apply`, necesitas pasar los valores de infraestructura a ContentFlow. + +### Script Automatizado: `configure-contentflow.sh` + +```bash +#!/bin/bash +# ============================================================================= +# configure-contentflow.sh +# Configura las variables de entorno de ContentFlow azd con los outputs +# de la Mini-AILZ desplegada con Terraform. +# ============================================================================= +set -euo pipefail + +# Verificar que estamos en el directorio correcto de Terraform +if [[ ! -f "ailz.tf" ]]; then + echo "ERROR: Ejecuta este script desde el directorio de Terraform Mini-AILZ" + exit 1 +fi + +# Directorio de ContentFlow (ajustar según tu setup) +CONTENTFLOW_DIR="${1:-../contentflow-test-001}" + +echo "Obteniendo outputs de Terraform..." +VNET_ID=$(terraform output -raw vnet_resource_id) +LAW_ID=$(terraform output -raw log_analytics_workspace_id) +RG_NAME=$(terraform output -raw 2>/dev/null | grep -oP 'resource-group \K[^ \\]+' || true) + +# Obtener el nombre real del resource group del state +RG_NAME=$(terraform show -json | python3 -c " +import sys, json +state = json.load(sys.stdin) +for r in state.get('values', {}).get('root_module', {}).get('child_modules', []): + for res in r.get('resources', []): + if res['type'] == 'azurerm_resource_group': + print(res['values']['name']) + break +" 2>/dev/null || echo "rg-mini-ailz-contentflow") + +echo "Resource Group: $RG_NAME" +echo "VNet ID: $VNET_ID" +echo "Log Analytics ID: $LAW_ID" + +echo "" +echo "Obteniendo Private DNS Zone IDs..." + +# Función helper para obtener DNS Zone ID +get_dns_zone_id() { + local zone_name=$1 + az network private-dns zone show \ + --resource-group "$RG_NAME" \ + --name "$zone_name" \ + --query id -o tsv 2>/dev/null || echo "" +} + +BLOB_DNS=$(get_dns_zone_id "privatelink.blob.core.windows.net") +COSMOS_DNS=$(get_dns_zone_id "privatelink.documents.azure.com") +APPCONFIG_DNS=$(get_dns_zone_id "privatelink.azconfig.io") +ACR_DNS=$(get_dns_zone_id "privatelink.azurecr.io") +COGNITIVE_DNS=$(get_dns_zone_id "privatelink.cognitiveservices.azure.com") +CONTAINERAPP_DNS=$(get_dns_zone_id "privatelink.azurecontainerapps.io") + +# Verificar que encontramos todas las zonas +MISSING=0 +for ZONE_VAR in BLOB_DNS COSMOS_DNS APPCONFIG_DNS ACR_DNS COGNITIVE_DNS CONTAINERAPP_DNS; do + if [[ -z "${!ZONE_VAR}" ]]; then + echo "⚠ WARNING: No se encontró DNS Zone para $ZONE_VAR" + MISSING=1 + fi +done + +if [[ $MISSING -eq 1 ]]; then + echo "" + echo "Algunas DNS Zones no se encontraron. Verifica con:" + echo " az network private-dns zone list --resource-group $RG_NAME -o table" + echo "" + echo "El módulo puede usar nombres diferentes. Ajusta manualmente si es necesario." +fi + +echo "" +echo "Configurando azd environment en: $CONTENTFLOW_DIR" + +pushd "$CONTENTFLOW_DIR" > /dev/null + +azd env set DEPLOYMENT_MODE ailz-integrated +azd env set EXISTING_VNET_RESOURCE_ID "$VNET_ID" +azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$LAW_ID" +[[ -n "$BLOB_DNS" ]] && azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID "$BLOB_DNS" +[[ -n "$COSMOS_DNS" ]] && azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID "$COSMOS_DNS" +[[ -n "$APPCONFIG_DNS" ]] && azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID "$APPCONFIG_DNS" +[[ -n "$ACR_DNS" ]] && azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID "$ACR_DNS" +[[ -n "$COGNITIVE_DNS" ]] && azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID "$COGNITIVE_DNS" +[[ -n "$CONTAINERAPP_DNS" ]] && azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$CONTAINERAPP_DNS" + +popd > /dev/null + +echo "" +echo "✅ Configuración completada. Ejecuta:" +echo " cd $CONTENTFLOW_DIR && azd up" +``` + +### Mapeo Completo de Outputs → Variables azd + +| Output Terraform / Comando az | Variable azd ContentFlow | Descripción | +|---|---|---| +| `module.ailz.virtual_network.id` | `EXISTING_VNET_RESOURCE_ID` | Resource ID de la VNet | +| `module.ailz.log_analytics_workspace_id` | `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | LAW compartido | +| `az ... privatelink.blob.core.windows.net` | `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | DNS Zone para Blob | +| `az ... privatelink.documents.azure.com` | `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | DNS Zone para Cosmos | +| `az ... privatelink.azconfig.io` | `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | DNS Zone para App Config | +| `az ... privatelink.azurecr.io` | `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | DNS Zone para ACR | +| `az ... privatelink.cognitiveservices.azure.com` | `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | DNS Zone para AI Services | +| `az ... privatelink.azurecontainerapps.io` | `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | DNS Zone para Container Apps | +| (contenido en `pe-subnet`) | `PRIVATE_ENDPOINT_SUBNET_NAME` | Default: `pe-subnet` | +| (contenido en `aca-env-subnet`) | `CONTAINER_APPS_SUBNET_NAME` | Default: `aca-env-subnet` | + +--- + +## 7. Fix Conocido: storage_use_azuread + +### El Problema + +El módulo crea Storage Accounts con `shared_access_key_enabled = false` (default seguro). Sin embargo, el provider `azurerm` por defecto usa **autenticación por key** para operaciones de Storage. Esto produce: + +``` +Error: StatusCode=403 -- Original Error: autorest/azure: Service returned an error. +Status=403 Code="AuthorizationFailure" +``` + +### La Solución + +Agregar `storage_use_azuread = true` en el bloque del provider: + +```hcl +provider "azurerm" { + features { ... } + storage_use_azuread = true # ← CRÍTICO +} +``` + +### Prerrequisito + +El usuario que ejecuta `terraform apply` debe tener el rol **Storage Blob Data Contributor** en la suscripción o en los Resource Groups correspondientes: + +```bash +# Asignar rol a tu usuario (una vez) +az role assignment create \ + --assignee "$(az ad signed-in-user show --query id -o tsv)" \ + --role "Storage Blob Data Contributor" \ + --scope "/subscriptions/$(az account show --query id -o tsv)" +``` + +--- + +## 8. Paso a Paso: Despliegue + +### 8.1 Preparar el Entorno + +```bash +# Crear directorio de trabajo +mkdir mini-ailz-contentflow && cd mini-ailz-contentflow + +# Crear los archivos Terraform (copiar de la sección 5) +# terraform.tf, variables.tf, main.tf, ailz.tf, outputs.tf +``` + +### 8.2 Inicializar Terraform + +```bash +terraform init +``` + +Esto descarga el módulo AVM y todos sus sub-módulos (~20 módulos). + +### 8.3 Validar y Planificar + +```bash +# Validar sintaxis +terraform validate + +# Ver plan de ejecución +terraform plan -out=tfplan +``` + +El plan debería mostrar aproximadamente **15-20 recursos** a crear (vs ~45+ con el módulo completo). + +### 8.4 Aplicar + +```bash +terraform apply tfplan +``` + +Tiempo estimado: **15-25 minutos** (Bastion y Container Apps Environment son los más lentos). + +### 8.5 Verificar Recursos Creados + +```bash +# Listar recursos en el RG +az resource list \ + --resource-group rg-mini-ailz-contentflow \ + --query "[].{name:name, type:type}" \ + -o table +``` + +Deberías ver: +- Virtual Network +- Subredes (varias, creadas por el módulo) +- NSGs +- Private DNS Zones (6+) +- Bastion Host + Public IP +- Container Apps Environment +- Log Analytics Workspace +- Key Vault +- Container Registry +- Storage Account +- App Configuration +- Cosmos DB Account + +### 8.6 Configurar ContentFlow + +```bash +# Opción A: Script automatizado +chmod +x configure-contentflow.sh +./configure-contentflow.sh /path/to/contentflow + +# Opción B: Manual +cd /path/to/contentflow +azd env set DEPLOYMENT_MODE ailz-integrated +azd env set EXISTING_VNET_RESOURCE_ID "$(terraform -chdir=/path/to/mini-ailz output -raw vnet_resource_id)" +# ... (ver sección 6 para el mapeo completo) +``` + +### 8.7 Desplegar ContentFlow + +```bash +cd /path/to/contentflow +azd up +``` + +--- + +## 9. Comparación de Costo: Full vs Mini + +### Despliegue Completo del Módulo (tu test anterior en standalone-test001) + +| Recurso | SKU/Tier | Costo Estimado/mes | +|---|---|---| +| AI Foundry Hub + BYOR | S0 | ~$0 (pago por uso) | +| GPT-4.1 Model | GlobalStandard x1 | ~$0-30 (por tokens) | +| AI Search | Standard, 2 replicas | **~$500** | +| App Gateway (WAF_v2) | 2 scale units | **~$350** | +| Azure Bastion | Standard, 3 AZs | **~$350** | +| Azure Firewall | Standard, 3 AZs | **~$250** | +| Build VM | Standard_B2s | **~$30** | +| APIM | Premium x3 | **~$2,100** | +| Container Apps Env | Consumption (ZR) | ~$0-20 | +| Cosmos DB (BYOR) | Serverless | ~$0-10 | +| Cosmos DB (GenAI) | Serverless | ~$0-10 | +| Key Vault x2 | Standard | ~$0-5 | +| Storage Account x2 | ZRS/GRS | ~$5-15 | +| Container Registry | Premium (ZR) | **~$60** | +| App Configuration | Standard | ~$36 | +| Log Analytics | Per-GB | ~$0-20 | +| Bing Grounding | G1 | ~$5 | +| KS AI Search | Standard | **~$250** | +| VNet + Subredes | - | ~$0 | +| Private DNS Zones | - | ~$1-5 | +| **TOTAL ESTIMADO** | | **~$3,500-4,000/mes** | + +### Mini-AILZ (este documento) + +| Recurso | SKU/Tier | Costo Estimado/mes | +|---|---|---| +| Azure Bastion | **Basic**, sin AZs | **~$140** | +| Container Apps Env | Consumption, sin ZR | ~$0-20 | +| Container Registry | Premium (requerido para PE) | **~$50** | +| Cosmos DB (GenAI) | Serverless | ~$0-10 | +| Key Vault | Standard | ~$0-5 | +| Storage Account | **LRS** | ~$2-5 | +| App Configuration | Standard | ~$36 | +| Log Analytics | Per-GB | ~$0-20 | +| VNet + Subredes | - | ~$0 | +| Private DNS Zones | - | ~$1-5 | +| NSGs | - | ~$0 | +| **TOTAL ESTIMADO** | | **~$230-290/mes** | + +### Ahorro + +| Métrica | Full | Mini | Ahorro | +|---|---|---|---| +| Costo mensual | ~$3,700 | ~$260 | **~$3,440/mes (93%)** | +| Recursos Azure | ~40+ | ~15-20 | **50% menos** | +| Tiempo deploy | ~45 min | ~20 min | **55% menos** | + +> **Nota**: Los costos de ContentFlow en sí (Container Apps, Cosmos DB, Storage, etc. que crea `azd up`) son adicionales y similares en ambos escenarios (~$100-200/mes dependiendo del uso). + +--- + +## 10. Diagrama de Arquitectura Resultante + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Resource Group: rg-mini-ailz-contentflow │ +│ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ VNet (10.0.0.0/16) │ │ +│ │ │ │ +│ │ ┌─────────────────┐ ┌──────────────────────────────────────┐ │ │ +│ │ │ Bastion Subnet │ │ Container Apps Subnet (/23) │ │ │ +│ │ │ ┌────────────┐ │ │ │ │ │ +│ │ │ │ Bastion │ │ │ ┌──────────────────────────────┐ │ │ │ +│ │ │ │ (Basic) │ │ │ │ Container Apps Environment │ │ │ │ +│ │ │ └────────────┘ │ │ │ (Internal Load Balancer) │ │ │ │ +│ │ └─────────────────┘ │ │ │ │ │ │ │ +│ │ │ │ │ ← ContentFlow despliega: │ │ │ │ +│ │ ┌─────────────────┐ │ │ │ API / Worker / Web │ │ │ │ +│ │ │ PE Subnet │ │ │ └──────────────────────────────┘ │ │ │ +│ │ │ │ │ └──────────────────────────────────────┘ │ │ +│ │ │ ← ContentFlow │ │ │ │ +│ │ │ crea PEs: │ │ ┌──────────────────────────────────────┐ │ │ +│ │ │ ● Blob Storage │ │ │ Plataforma AILZ (módulo) │ │ │ +│ │ │ ● Cosmos DB │ │ │ │ │ │ +│ │ │ ● App Config │ │ │ ● Log Analytics Workspace │ │ │ +│ │ │ ● ACR │ │ │ ● Key Vault (GenAI) │ │ │ +│ │ │ ● AI Foundry │ │ │ ● Container Registry (GenAI) │ │ │ +│ │ └─────────────────┘ │ │ ● Storage Account (GenAI, LRS) │ │ │ +│ │ │ │ ● App Configuration (GenAI) │ │ │ +│ └───────────────────────│ │ ● Cosmos DB (GenAI) │ │ │ +│ │ │ ● NSGs │ │ │ +│ ┌────────────────────┐ │ └──────────────────────────────────────┘ │ │ +│ │ Private DNS Zones │ │ │ │ +│ │ (auto-vinculadas) │ └────────────────────────────────────────────┘ │ +│ │ │ │ +│ │ ● privatelink.blob.core.windows.net │ +│ │ ● privatelink.documents.azure.com │ +│ │ ● privatelink.azconfig.io │ +│ │ ● privatelink.azurecr.io │ +│ │ ● privatelink.cognitiveservices.azure.com │ +│ │ ● privatelink.azurecontainerapps.io │ +│ │ ● (+ otras del módulo: vault, openai, etc.) │ +│ └────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────┐ +│ Resource Group: rg-contentflow-ailz (creado por azd up) │ +│ │ +│ ● Storage Account (Blob + Queue) + Private Endpoints en pe-subnet │ +│ ● Cosmos DB + Private Endpoint en pe-subnet │ +│ ● App Configuration + Private Endpoint en pe-subnet │ +│ ● ACR Premium + Private Endpoint en pe-subnet │ +│ ● AI Foundry (Cognitive Services) + Private Endpoint en pe-subnet │ +│ ● Container Apps (API, Worker, Web) → Container Apps Environment │ +│ ● Application Insights → Log Analytics Workspace (compartido) │ +│ ● User Assigned Managed Identity + Role Assignments │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 11. Notas de Operación y Troubleshooting + +### 11.1 Nombres de Subredes + +El módulo AVM genera nombres de subredes internamente. ContentFlow espera `pe-subnet` y `aca-env-subnet`. Si los nombres generados por el módulo no coinciden, puedes: + +1. **Verificar nombres reales** después del apply: + ```bash + terraform output subnets + ``` + +2. **Configurar en ContentFlow** los nombres reales: + ```bash + azd env set PRIVATE_ENDPOINT_SUBNET_NAME "" + azd env set CONTAINER_APPS_SUBNET_NAME "" + ``` + +3. **Override en vnet_definition** (si el módulo lo permite): + ```hcl + vnet_definition = { + address_space = ["10.0.0.0/16"] + subnets = { + pe-subnet = { + name = "pe-subnet" + address_prefix = "10.0.1.0/27" + } + aca-env-subnet = { + name = "aca-env-subnet" + address_prefix = "10.0.16.0/23" + } + } + } + ``` + +### 11.2 Container Apps Environment Dual + +Si el módulo crea un Container Apps Environment pero ContentFlow también intenta crear uno durante `azd up`, podrías terminar con dos CAEs. Para evitar esto: + +- **Opción A**: Configurar ContentFlow para usar el CAE existente (pasar su ID como variable de entorno) +- **Opción B**: Deshabilitar el CAE en el módulo (`deploy = false` en `container_app_environment_definition`) y dejar que ContentFlow lo cree + +### 11.3 Error: "Address space conflict" + +Si ves errores sobre conflictos de rango de IP: + +``` +Error: creating Virtual Network: the address space "192.168.0.0/20" overlaps with... +``` + +Asegúrate de usar un rango que **no esté en uso** en tu suscripción. Recomendamos `10.0.0.0/16` en lugar de `192.168.0.0/x`. + +### 11.4 Error: "Insufficient subnet size" + +Container Apps Environment requiere una subred de al menos `/23` (512 IPs). El módulo normalmente calcula esto correctamente, pero si haces override de subredes, asegúrate de respetar este mínimo. + +### 11.5 Terraform Destroy: Orden de Dependencias + +Para destruir la Mini-AILZ: + +```bash +# PRIMERO: Destruir ContentFlow (tiene dependencias en la red) +cd /path/to/contentflow +azd down --force --purge + +# SEGUNDO: Destruir la Mini-AILZ +cd /path/to/mini-ailz +terraform destroy +``` + +> **Importante**: Si intentas destruir la Mini-AILZ mientras ContentFlow tiene Private Endpoints activos en las subredes, el destroy fallará. Siempre destruye ContentFlow primero. + +### 11.6 Restricción de Address Space para Foundry CapabilityHost + +Si alguna vez necesitas habilitar AI Foundry con agent service (`capabilityHost`), hay una restricción conocida: + +> El address space de la VNet **no puede ser** `192.168.0.0/16` (pero sí puede ser ranges dentro de él como `192.168.0.0/20`). Otros rangos RFC1918 como `10.0.0.0/8` y `172.16.0.0/12` **sí funcionan correctamente**. + +Nuestro rango `10.0.0.0/16` no tiene este problema. + +--- + +## 12. Limpieza de Recursos + +### Limpieza Completa (ContentFlow + Mini-AILZ) + +```bash +# 1. Destruir ContentFlow primero +cd /path/to/contentflow +azd down --force --purge + +# 2. Destruir Mini-AILZ +cd /path/to/mini-ailz +terraform destroy -auto-approve + +# 3. Verificar que el RG fue eliminado +az group show --name rg-mini-ailz-contentflow 2>/dev/null && echo "RG aún existe" || echo "RG eliminado" +``` + +### Limpieza Solo ContentFlow (mantener Mini-AILZ) + +```bash +# Solo destruir ContentFlow, la red permanece para reusar +cd /path/to/contentflow +azd down --force --purge +``` + +### Purge de Recursos con Soft-Delete + +Algunos recursos Azure tienen soft-delete. Si necesitas re-crear con el mismo nombre: + +```bash +# Key Vault +az keyvault purge --name + +# Cognitive Services +az cognitiveservices account purge \ + --location \ + --resource-group \ + --name + +# App Configuration +az appconfig purge --name +``` + +--- + +## Referencia Rápida + +### Versiones Utilizadas + +| Componente | Versión | +|---|---| +| Terraform | >= 1.9, < 2.0 | +| AVM Pattern Module | 0.4.0 | +| azurerm provider | >= 3.116, < 5.0 | +| azapi provider | ~> 2.0 | +| ContentFlow | Modo `ailz-integrated` | + +### Documentos Relacionados + +- [01-overview.md](01-overview.md) — Visión general de ContentFlow +- [02-architecture-detailed.md](02-architecture-detailed.md) — Arquitectura técnica detallada +- [03-use_cases_examples.md](03-use_cases_examples.md) — Casos de uso y ejemplos +- [04-ai_lz_options.md](04-ai_lz_options.md) — Guía de despliegue AILZ (Azure CLI manual) +- **[Módulo AVM en GitHub](https://github.com/Azure/terraform-azurerm-avm-ptn-aiml-landing-zone)** — Código fuente y documentación completa +- **[Módulo AVM en Terraform Registry](https://registry.terraform.io/modules/Azure/avm-ptn-aiml-landing-zone/azurerm/latest)** — Registry oficial + +--- + +> **Última actualización**: Julio 2025 +> **Módulo fuente**: `Azure/avm-ptn-aiml-landing-zone/azurerm` v0.4.0 +> **Autor**: Análisis técnico para demos ContentFlow con suscripción única diff --git a/Analysis/06-service-health.md b/Analysis/06-service-health.md new file mode 100644 index 0000000..719b6de --- /dev/null +++ b/Analysis/06-service-health.md @@ -0,0 +1,825 @@ +# Validación de Salud de Servicios en ContentFlow + +> **Documento técnico** — Arquitectura completa del sistema de health checks, flujo de validación servicio por servicio, y configuraciones necesarias para que funcione correctamente en modo `ailz-integrated` (conectividad privada). + +--- + +## Tabla de Contenidos + +1. [Resumen del Sistema de Health Checks](#1-resumen-del-sistema-de-health-checks) +2. [Arquitectura del Flujo de Validación](#2-arquitectura-del-flujo-de-validación) +3. [Componente Web: Footer y Diálogo de System Health](#3-componente-web-footer-y-diálogo-de-system-health) +4. [API: Router de Health y Endpoint `/api/health`](#4-api-router-de-health-y-endpoint-apihealth) +5. [API: HealthService — Lógica de Validación por Servicio](#5-api-healthservice--lógica-de-validación-por-servicio) + - [5.1 App Configuration](#51-app-configuration) + - [5.2 Cosmos DB](#52-cosmos-db) + - [5.3 Blob Storage](#53-blob-storage) + - [5.4 Storage Queue](#54-storage-queue) + - [5.5 Worker Engine](#55-worker-engine) +6. [Worker: Validación de Startup y Endpoint `/status`](#6-worker-validación-de-startup-y-endpoint-status) +7. [Container Apps: Liveness Probes (Infraestructura)](#7-container-apps-liveness-probes-infraestructura) +8. [Configuración de la Variable `VITE_API_BASE_URL`](#8-configuración-de-la-variable-vite_api_base_url) +9. [Diagnóstico en Modo AILZ-Integrated](#9-diagnóstico-en-modo-ailz-integrated) + - [9.1 Problema: Todos los Servicios Aparecen Offline](#91-problema-todos-los-servicios-aparecen-offline) + - [9.2 Problema: API Conectada pero Servicios Backend en Error](#92-problema-api-conectada-pero-servicios-backend-en-error) + - [9.3 Problema: Worker Engine en Error](#93-problema-worker-engine-en-error) +10. [Requisitos de Conectividad por Servicio](#10-requisitos-de-conectividad-por-servicio) +11. [Configuraciones Requeridas para AILZ](#11-configuraciones-requeridas-para-ailz) +12. [Comandos de Verificación Manual](#12-comandos-de-verificación-manual) + +--- + +## 1. Resumen del Sistema de Health Checks + +ContentFlow implementa un sistema de validación de salud en **tres niveles**: + +| Nivel | Componente | Propósito | Frecuencia | +|---|---|---|---| +| **UI (Frontend)** | `Footer.tsx` | Muestra estado visual de cada servicio al usuario | Cada 10 minutos + al cargar | +| **API (Backend)** | `HealthService` | Valida conectividad real a cada servicio Azure | Bajo demanda (llamada de la UI) | +| **Infraestructura** | Liveness Probes | Container Apps valida que los containers estén vivos | Cada 60 segundos | + +### Servicios Validados + +El sistema valida **6 componentes**: + +| Servicio | Qué Valida | Método de Validación | +|---|---|---| +| **API Service** | Que el frontend puede alcanzar el API | HTTP GET a `/api/health` | +| **App Configuration** | Conectividad al store de configuración | Listar keys con prefijo `contentflow.app.*` | +| **Cosmos DB** | Conectividad a la base de datos | Leer propiedades del database | +| **Blob Storage** | Acceso al container de documentos | Obtener propiedades del container | +| **Storage Queue** | Acceso a la cola de trabajo | Obtener propiedades de la queue | +| **Worker Engine** | Que el worker está corriendo | HTTP GET a `https://{worker-fqdn}/status` | + +### Estados Posibles + +| Estado | Indicador Visual | Significado | +|---|---|---| +| **connected** | 🟢 Verde | Todos los servicios responden correctamente | +| **degraded** | 🟡 Amarillo | Algunos servicios están con error pero no todos | +| **offline** | 🔴 Rojo | Todos los servicios con error o API inalcanzable | + +--- + +## 2. Arquitectura del Flujo de Validación + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ NAVEGADOR │ +│ │ +│ Footer.tsx │ +│ ┌─────────────────┐ │ +│ │ System Health ● │ ← Indicador visual en el footer │ +│ └────────┬────────┘ │ +│ │ click │ +│ ┌────────▼────────┐ │ +│ │ Dialog con 6 │ ← Muestra detalle por servicio │ +│ │ ServiceHealth │ │ +│ │ Items │ │ +│ └────────┬────────┘ │ +│ │ HTTP GET │ +│ │ {VITE_API_BASE_URL}/health │ +└───────────┼─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ API Container App (api-{token}) │ +│ Puerto: 8090 │ +│ │ +│ FastAPI Router: /api/health │ +│ ┌──────────────────────────────┐ │ +│ │ health_router.get("/") │ │ +│ │ → HealthService │ │ +│ │ .check_all_services() │ │ +│ └──────────┬───────────────────┘ │ +│ │ asyncio.gather (5 checks en paralelo) │ +│ │ │ +│ ┌──────────▼───────────────────────────────────────────┐ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌──────────┐ ┌───────────────┐ │ │ +│ │ │ App Config │ │ Cosmos DB│ │ Blob Storage │ │ │ +│ │ │ list keys │ │ read db │ │ get container │ │ │ +│ │ └──────┬──────┘ └────┬─────┘ └──────┬────────┘ │ │ +│ │ │ │ │ │ │ +│ │ ┌──────┴──────┐ ┌───┴────────────┐ │ │ +│ │ │ Storage Q │ │ Worker Engine │ │ │ +│ │ │ get props │ │ GET /status │ │ │ +│ │ └──────┬──────┘ └───┬────────────┘ │ │ +│ └─────────┼──────────────┼───────────────────────────────┘ │ +└────────────┼──────────────┼──────────────────────────────────────────────┘ + │ │ + ▼ ▼ +┌────────────────┐ ┌─────────────────────────────────────┐ +│ Azure Services │ │ Worker Container App (worker-{token})│ +│ (via PE en │ │ Puerto: 8099 │ +│ AILZ mode) │ │ GET /status → WorkerStatusResponse │ +└────────────────┘ └─────────────────────────────────────┘ +``` + +--- + +## 3. Componente Web: Footer y Diálogo de System Health + +**Archivo:** `contentflow-web/src/components/Footer.tsx` + +### Cómo Funciona + +1. El componente `Footer` se monta al cargar la aplicación +2. Después de **5 segundos** de espera inicial, ejecuta el primer health check +3. Luego repite el check cada **10 minutos** (600,000 ms) +4. El usuario puede forzar un refresh manual desde el diálogo + +### Flujo del Check + +```typescript +// Footer.tsx — Lógica principal +const checkHealth = async () => { + try { + const healthData = await getHealthCheck(); // GET {VITE_API_BASE_URL}/health + + // Si llegamos aquí, el API es alcanzable + setSystemHealth({ + api: "connected", + systemOverall: healthData.status, // "connected" | "degraded" | "offline" + appConfig: healthData.services.app_config?.status, + cosmosDB: healthData.services.cosmos_db?.status, + blobStorage: healthData.services.blob_storage?.status, + storageQueue: healthData.services.storage_queue?.status, + worker: healthData.services.worker?.status, + serviceDetails: { ... } // Detalles expandibles por servicio + }); + } catch (error) { + // Si el fetch falla → TODO se marca como offline + // Esto significa que el navegador no puede alcanzar el API + setSystemHealth({ api: "offline", systemOverall: "offline", ... }); + } +}; +``` + +### Qué Muestra el Diálogo + +Cada servicio se muestra como un item expandible (`ServiceHealthItem`) que incluye: + +- **Nombre del servicio** y indicador de color (verde/rojo) +- **Endpoint** consultado (expandible) +- **Tiempo de respuesta** en milisegundos +- **Mensaje** de resultado +- **Error** detallado si falló (en rojo) +- **Detalles** adicionales (JSON con metadatos del servicio) +- **Última verificación** (timestamp) + +### URL de Conexión al API + +La Web UI construye la URL del API usando la variable de entorno **`VITE_API_BASE_URL`**: + +```typescript +// apiClient.ts +const defaultConfig: ApiConfig = { + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8090/api/', +}; +``` + +**Esto es crítico en modo AILZ** — ver [sección 8](#8-configuración-de-la-variable-vite_api_base_url). + +--- + +## 4. API: Router de Health y Endpoint `/api/health` + +**Archivo:** `contentflow-api/app/routers/health.py` + +### Endpoints Disponibles + +| Endpoint | Método | Descripción | +|---|---|---| +| `GET /api/health/` | GET | Validación completa de **todos** los servicios | +| `GET /api/health/{service_name}` | GET | Validación de un servicio específico | + +### Respuesta del Endpoint `/api/health/` + +```json +{ + "status": "connected | degraded | error", + "services": { + "app_config": { + "name": "app_config", + "status": "connected | error", + "message": "Connected successfully", + "error": null, + "details": { "config_items_count": 5, "credential_type": "azure_credential" }, + "response_time_ms": 120, + "last_checked": "2026-03-28T10:30:00Z", + "endpoint": "https://appcs-r7b6gtdiob4em.azconfig.io" + }, + "cosmos_db": { "..." }, + "blob_storage": { "..." }, + "storage_queue": { "..." }, + "worker": { "..." } + }, + "checked_at": "2026-03-28T10:30:00Z", + "summary": { "connected": 5, "error": 0, "total": 5 } +} +``` + +### Lógica de Estado General + +```python +# Determinación del estado overall +statuses = [service.status for service in services.values()] +if all(status == "connected" for status in statuses): + overall_status = "connected" # TODO bien → 🟢 +elif all(status == "error" for status in statuses): + overall_status = "error" # TODO mal → 🔴 +else: + overall_status = "degraded" # Algunos bien, algunos mal → 🟡 +``` + +### Inyección de Dependencias + +El `HealthService` se construye con los valores de configuración de App Config: + +```python +# dependencies.py +def get_health_service(): + app_settings = get_settings() + return HealthService( + cosmos_endpoint=app_settings.COSMOS_DB_ENDPOINT, + cosmos_db_name=app_settings.COSMOS_DB_NAME, + blob_storage_account=app_settings.BLOB_STORAGE_ACCOUNT_NAME, + blob_storage_container=app_settings.BLOB_STORAGE_CONTAINER_NAME, + storage_account_worker_queue_url=app_settings.STORAGE_ACCOUNT_WORKER_QUEUE_URL, + storage_worker_queue_name=app_settings.STORAGE_WORKER_QUEUE_NAME, + worker_engine_api_endpoint=app_settings.WORKER_ENGINE_API_ENDPOINT + ) +``` + +Estos valores se cargan de **Azure App Configuration** al iniciar la API, usando las keys con prefijo `contentflow.common.*` y `contentflow.api.*`. + +--- + +## 5. API: HealthService — Lógica de Validación por Servicio + +**Archivo:** `contentflow-api/app/services/health_service.py` + +Todos los checks se ejecutan **en paralelo** usando `asyncio.gather()`, lo que minimiza el tiempo de respuesta total. + +--- + +### 5.1 App Configuration + +**Método:** `_check_app_config_health()` + +**Qué valida:** +1. Que `AZURE_APP_CONFIG_ENDPOINT` o `AZURE_APP_CONFIG_CONNECTION_STRING` existan como variable de entorno +2. Que se pueda conectar al store +3. Que se puedan listar keys con filtro `contentflow.app.*` + +**Cómo se conecta:** +- Si existe `AZURE_APP_CONFIG_CONNECTION_STRING` → usa connection string +- Si existe `AZURE_APP_CONFIG_ENDPOINT` → usa `DefaultAzureCredential` (Managed Identity) + +**Qué puede fallar en AILZ:** +- App Config tiene `publicNetworkAccess: Disabled` +- La API container app necesita resolver `appcs-{token}.azconfig.io` → IP privada del Private Endpoint +- Si la Private DNS Zone `privatelink.azconfig.io` no tiene el A record correcto, falla + +**Ejemplo de error típico:** +```json +{ + "status": "error", + "message": "Connection failed: ServiceRequestError", + "error": "Cannot connect to host appcs-r7b6gtdiob4em.azconfig.io:443" +} +``` + +--- + +### 5.2 Cosmos DB + +**Método:** `_check_cosmos_db_health()` + +**Qué valida:** +1. Que `COSMOS_DB_ENDPOINT` esté configurado (no vacío) +2. Conectividad al endpoint de Cosmos DB +3. Que el database especificado (`contentflow`) exista +4. Lectura de propiedades del database (operación de solo lectura) + +**Cómo se conecta:** +- Usa `CosmosClient` con `DefaultAzureCredential` (Managed Identity del Container App) +- Endpoint: `https://cosmos-{token}.documents.azure.com:443/` + +**Qué puede fallar en AILZ:** +- Cosmos DB tiene `publicNetworkAccess: Disabled` +- Requiere Private Endpoint resolviendo a IP privada via `privatelink.documents.azure.com` +- Requiere que la Managed Identity tenga rol `Cosmos DB Built-in Data Contributor` + +**Ejemplo de error típico:** +```json +{ + "status": "error", + "message": "Connection failed: CosmosHttpResponseError", + "error": "Request blocked by network rules" +} +``` + +--- + +### 5.3 Blob Storage + +**Método:** `_check_blob_storage_health()` + +**Qué valida:** +1. Que `BLOB_STORAGE_ACCOUNT_NAME` y `BLOB_STORAGE_CONTAINER_NAME` estén configurados +2. Conectividad a la cuenta de storage +3. Que el container `content` exista +4. Lee propiedades del container (last_modified, lease_status) + +**Cómo se conecta:** +- Construye URL: `https://{account}.blob.core.windows.net` +- Usa `BlobServiceClient` con `DefaultAzureCredential` + +**Qué puede fallar en AILZ:** +- Storage tiene `publicNetworkAccess: Disabled` +- Requiere Private Endpoint blob resolviendo via `privatelink.blob.core.windows.net` +- Requiere rol `Storage Blob Data Contributor` en la Managed Identity + +--- + +### 5.4 Storage Queue + +**Método:** `_check_storage_queue_health()` + +**Qué valida:** +1. Que `STORAGE_ACCOUNT_WORKER_QUEUE_URL` esté configurado +2. Conectividad al servicio de colas +3. Que la queue `contentflow-execution-requests` exista +4. Lee propiedades de la queue (approximate_message_count) + +**Cómo se conecta:** +- Usa `QueueServiceClient` con `DefaultAzureCredential` +- URL: `https://{account}.queue.core.windows.net` + +**Qué puede fallar en AILZ:** +- Requiere Private Endpoint queue resolviendo via `privatelink.queue.core.windows.net` +- Si la DNS Zone Group del queue PE no se creó correctamente, no hay A record +- Requiere rol `Storage Queue Data Contributor` + +**Nota importante:** La Private DNS Zone para queue (`privatelink.queue.core.windows.net`) puede no haberse pasado al Bicep si `existingQueuePrivateDnsZoneId` no estaba en `main.parameters.json`. Es un gap conocido — ver [sección 11](#11-configuraciones-requeridas-para-ailz). + +--- + +### 5.5 Worker Engine + +**Método:** `_check_worker_health()` + +**Qué valida:** +1. Que el Worker Container App esté corriendo y responda +2. Llama a `GET https://{worker-fqdn}/status` +3. Valida que el response tenga status 200 +4. Extrae: `running`, `worker_name`, `processing_workers`, `source_workers` + +**Cómo se conecta:** +- Usa `aiohttp` para hacer HTTP GET al FQDN interno del Worker +- Endpoint configurado en App Config: `contentflow.api.WORKER_ENGINE_API_ENDPOINT` +- Valor típico: `https://worker-{token}.internal.{domain}.azurecontainerapps.io` +- Timeout: 5 segundos + +**Qué puede fallar en AILZ:** +- El API Container App necesita resolver el FQDN interno del Worker Container App +- En AILZ, los FQDNs tienen formato `*.internal.{defaultDomain}`, resolubles solo dentro del CAE o del VNet con Private DNS Zone +- La comunicación entre Container Apps **dentro del mismo CAE** funciona sin necesidad de DNS Zone externa — el CAE resuelve internamente +- Si la key `WORKER_ENGINE_API_ENDPOINT` en App Config tiene un FQDN incorrecto, falla + +**Ejemplo de error típico:** +```json +{ + "status": "error", + "message": "Worker health check failed: ClientConnectorError", + "error": "Cannot connect to host worker-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io:443" +} +``` + +--- + +## 6. Worker: Validación de Startup y Endpoint `/status` + +**Archivos:** `contentflow-worker/app/startup.py`, `contentflow-worker/app/api.py` + +### Validación al Iniciar (StartupValidator) + +Antes de aceptar trabajo, el Worker ejecuta 4 validaciones: + +| Check | Qué Valida | Consecuencia si Falla | +|---|---|---| +| **Settings Validation** | Que todas las configuraciones requeridas existen | Worker no inicia | +| **Cosmos DB Connectivity** | Conexión al endpoint, database existe | Worker no inicia | +| **Cosmos DB Containers** | Que los 5 containers requeridos existen | Worker no inicia | +| **Storage Queue Connectivity** | Conexión a la queue, queue existe | Worker no inicia | + +Si cualquier check falla, el Worker **no inicia** y loguea los errores detallados. + +### Health Endpoints del Worker + +| Endpoint | Descripción | +|---|---| +| `GET /` | Info básica del servicio | +| `GET /health` | Health check simple (solo verifica que el API responde) | +| `GET /status` | Estado detallado del engine (workers running, counts) | + +El endpoint `/status` es el que usa el API `HealthService` para validar el Worker. + +### Monitoreo Interno de Workers + +El `WorkerEngine` tiene un loop que cada **30 segundos** verifica si los worker processes siguen vivos: + +```python +def _check_worker_health(self): + # Si un processing worker murió, lo reinicia + for i, worker in enumerate(self.processing_workers): + if not worker.is_alive() and not self.stop_event.is_set(): + new_worker = self._create_processing_worker(i) + new_worker.start() + + # Lo mismo para source workers +``` + +--- + +## 7. Container Apps: Liveness Probes (Infraestructura) + +**Archivo:** `infra/bicep/modules/container-app.bicep` + +### Configuración del Liveness Probe + +Cada Container App tiene un probe HTTP configurado por Bicep: + +```bicep +probes: [ + { + type: 'Liveness' + httpGet: { + path: livenessProbePath // '/' para los 3 servicios + port: targetPort // 8090 (API), 8099 (Worker), 8080 (Web) + } + initialDelaySeconds: 5 // Espera 5s antes del primer check + periodSeconds: 60 // Verifica cada 60 segundos + } +] +``` + +| Container App | Probe Path | Puerto | Qué Responde | +|---|---|---|---| +| API (`api-{token}`) | `/` | 8090 | FastAPI root endpoint | +| Worker (`worker-{token}`) | `/` | 8099 | FastAPI root endpoint | +| Web (`web-{token}`) | `/` | 8080 | nginx sirve `index.html` | + +### Diferencia con el Health Check de la UI + +- **Liveness Probe**: Lo ejecuta Azure Container Apps. Si falla 3 veces consecutivas, reinicia el container. Solo verifica que el proceso responde. +- **Health Check de la UI**: Lo ejecuta el navegador del usuario → API → servicios backend. Verifica conectividad a cada servicio Azure. + +--- + +## 8. Configuración de la Variable `VITE_API_BASE_URL` + +Esta variable es **el puente fundamental** entre la Web UI y el API. Si está mal configurada, todos los servicios aparecen offline. + +### Cómo se Configura + +**En Bicep** (al crear el Web Container App): +```bicep +// main.bicep, línea ~663 +environmentVariables: [ + { + name: 'VITE_API_BASE_URL' + value: 'https://${apiContainerApp.outputs.fqdn}/api/' + } +] +``` + +**En el Dockerfile** (runtime replacement): +```dockerfile +# Build time: se escribe placeholder +RUN echo "VITE_API_BASE_URL=API_URL" > .env + +# Runtime: entrypoint.sh reemplaza el placeholder en los JS compilados +sed -i "s|API_URL|${VITE_API_BASE_URL:-}|g" "$file" +``` + +### Valor en Modo AILZ + +En modo `ailz-integrated`, el FQDN del API tiene formato interno: +``` +https://api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/api/ +``` + +**El problema:** Este FQDN solo es resolvible dentro del VNet (o si existe la Private DNS Zone del CAE). Cuando el usuario abre la Web UI en su navegador: + +1. El **navegador** (corriendo en el JumpBox o máquina del usuario) intenta hacer `fetch()` a esa URL +2. La resolución DNS de ese FQDN debe funcionar **desde la máquina del navegador** +3. Si la Private DNS Zone para `delightfulwave-2b288efe.eastus2.azurecontainerapps.io` no existe o no está vinculada al VNet → DNS falla → todo aparece offline + +### Con Application Gateway + +Cuando hay un App Gateway como intermediario, el flujo cambia: + +``` +Navegador → App Gateway IP → /api/* → API Container App (interno) +``` + +En este escenario, `VITE_API_BASE_URL` debería apuntar al **App Gateway**, no directamente al API Container App. Esto requiere: + +1. El App Gateway tiene una IP (pública o privada) accesible desde el navegador +2. La Web UI está configurada para llamar al App Gateway +3. El App Gateway hace path-based routing: `/api/*` → API backend + +**Opciones de configuración:** + +| Escenario | `VITE_API_BASE_URL` | Funciona Desde | +|---|---|---| +| AILZ sin App Gateway | `https://api-{token}.internal.{domain}/api/` | Solo JumpBox (si DNS resuelve) | +| AILZ con App Gateway (IP privada) | `https://{appgw-private-ip}/api/` o dominio custom | Dentro del VNet | +| AILZ con App Gateway (IP pública) | `https://{dominio-publico}/api/` | Cualquier lugar | +| Relativo (recomendado con AppGW) | `/api/` | Donde sea que el usuario acceda | + +**Recomendación para AILZ con App Gateway:** Usar path relativo `/api/` como `VITE_API_BASE_URL`, así el navegador usa el mismo host por el que accedió a la web. Esto funciona porque el App Gateway enruta `/*` → Web y `/api/*` → API. + +--- + +## 9. Diagnóstico en Modo AILZ-Integrated + +### 9.1 Problema: Todos los Servicios Aparecen Offline + +**Síntoma:** El indicador en el footer está 🔴, el diálogo muestra todo en rojo. + +**Causa más probable:** El navegador **no puede alcanzar** el API Container App. + +**Verificación:** + +```bash +# Desde el JumpBox, verificar que el FQDN del API resuelve +nslookup api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io + +# Si NO resuelve → falta Private DNS Zone para el dominio del CAE +# Si SÍ resuelve → verificar que el API responde +curl -k https://api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/api/health +``` + +**Causa raíz:** No existe Private DNS Zone para `delightfulwave-2b288efe.eastus2.azurecontainerapps.io` (el dominio dinámico del CAE) vinculada al VNet. + +**Solución:** El equipo de plataforma debe crear: +1. Private DNS Zone: `delightfulwave-2b288efe.eastus2.azurecontainerapps.io` +2. A record: `*` → `10.0.195.200` (CAE static IP) +3. VNet link al VNet del AILZ + +**Nota:** La DNS Zone `privatelink.azurecontainerapps.io` que ya existe en el AILZ es para el **management plane** de Container Apps, no para los FQDNs de las aplicaciones. Los FQDNs de las apps usan el dominio dinámico del CAE. + +--- + +### 9.2 Problema: API Conectada pero Servicios Backend en Error + +**Síntoma:** API está 🟢, pero Cosmos DB, Storage, App Config están 🔴. + +**Causa más probable:** Los Private Endpoints no tienen A records en las DNS Zones, o la Managed Identity no tiene los roles RBAC correctos. + +**Verificación:** + +```bash +# Verificar que los A records existen en cada DNS Zone +az network private-dns record-set a list \ + -g {AILZ_RG} -z "privatelink.documents.azure.com" -o table + +az network private-dns record-set a list \ + -g {AILZ_RG} -z "privatelink.blob.core.windows.net" -o table + +az network private-dns record-set a list \ + -g {AILZ_RG} -z "privatelink.azconfig.io" -o table +``` + +**Si faltan A records:** Los DNS Zone Groups no se crearon correctamente. Los módulos en `main.bicep` incluyen DNS Zone Groups explícitos como workaround por incompatibilidades de AVM. Verificar que se ejecutaron: + +```bash +# Verificar DNS Zone Groups de private endpoints +az network private-endpoint dns-zone-group list \ + --endpoint-name "{storageAccountName}-blob-pe" \ + -g {CF_RG} -o table +``` + +**Si los A records existen pero sigue fallando:** Verificar RBAC: + +```bash +# Listar roles de la Managed Identity +az role assignment list \ + --assignee {MANAGED_IDENTITY_PRINCIPAL_ID} \ + --scope "/subscriptions/{sub}/resourceGroups/{CF_RG}" \ + -o table +``` + +--- + +### 9.3 Problema: Worker Engine en Error + +**Síntoma:** Todos los servicios están 🟢 excepto Worker Engine que está 🔴. + +**Causa más probable:** El API Container App no puede alcanzar el Worker Container App, o el valor de `WORKER_ENGINE_API_ENDPOINT` en App Config es incorrecto. + +**Verificación:** + +```bash +# Verificar qué valor tiene WORKER_ENGINE_API_ENDPOINT en App Config +az appconfig kv show \ + --name {APP_CONFIG_NAME} \ + --key "contentflow.api.WORKER_ENGINE_API_ENDPOINT" \ + --auth-mode login \ + -o tsv --query value +``` + +**Debe ser:** `https://worker-{token}.internal.{defaultDomain}` (en modo AILZ el FQDN incluye `.internal.`). + +**Si el valor es correcto:** La comunicación entre containers del mismo CAE es interna y no requiere DNS Zone externa. Verificar que el Worker está corriendo: + +```bash +# Ver logs del Worker +az containerapp logs show \ + -n worker-{token} -g {CF_RG} --type system + +# Ver si el container está healthy +az containerapp show -n worker-{token} -g {CF_RG} \ + --query "properties.runningStatus" +``` + +**Nota sobre post-provision.sh:** En modo AILZ, la key `WORKER_ENGINE_API_ENDPOINT` se configura via `post-provision.sh` que corre desde el JumpBox. Este script toma el valor de `azd env get-value WORKER_ENDPOINT` y lo sube a App Config. Si `post-provision.sh` no corrió o se saltó, esta key puede estar vacía o con un valor incorrecto. + +--- + +## 10. Requisitos de Conectividad por Servicio + +Mapa completo de lo que necesita cada health check para funcionar en modo AILZ: + +| Servicio | Endpoint que Contacta | DNS Zone Requerida | RBAC Requerido | PE Name | +|---|---|---|---|---| +| App Config | `appcs-{token}.azconfig.io` | `privatelink.azconfig.io` | `App Configuration Data Reader` | `{appcs}-pe` | +| Cosmos DB | `cosmos-{token}.documents.azure.com` | `privatelink.documents.azure.com` | `Cosmos DB Built-in Data Contributor` | `{cosmos}-pe` | +| Blob Storage | `{storage}.blob.core.windows.net` | `privatelink.blob.core.windows.net` | `Storage Blob Data Contributor` | `{storage}-blob-pe` | +| Storage Queue | `{storage}.queue.core.windows.net` | `privatelink.queue.core.windows.net` | `Storage Queue Data Contributor` | `{storage}-queue-pe` | +| Worker | `worker-{token}.internal.{domain}` | N/A (intra-CAE) | N/A | N/A | +| API (desde Web) | `api-{token}.internal.{domain}` | `{defaultDomain}` del CAE | N/A | N/A | + +### Cadena Completa para que el Health Check Funcione + +``` +1. Navegador → puede resolver FQDN del API (o IP del App Gateway) +2. Navegador → HTTPS al API container +3. API → puede resolver endpoints de cada servicio Azure via Private DNS +4. API → Managed Identity tiene roles RBAC para leer cada servicio +5. API → puede resolver FQDN del Worker (intra-CAE, automático) +6. Worker → está corriendo y respondiendo en /status +``` + +Si **cualquier** eslabón de esta cadena falla, el servicio aparece en error. + +--- + +## 11. Configuraciones Requeridas para AILZ + +### Checklist de Configuración + +#### Infraestructura (Bicep/equipo de plataforma) + +- [ ] Private Endpoint creado para cada servicio (Storage blob, Storage queue, Cosmos, App Config, ACR) +- [ ] DNS Zone Group existente en cada Private Endpoint (crea A record automáticamente) +- [ ] Private DNS Zones vinculadas al VNet del AILZ +- [ ] Private DNS Zone para el dominio dinámico del CAE (`{defaultDomain}`) +- [ ] Wildcard A record (`*`) apuntando al CAE static IP en la DNS Zone del CAE +- [ ] Container Apps Environment configurado como `internal: true` +- [ ] Managed Identity con roles RBAC necesarios asignados + +#### App Configuration Keys (post-provision.sh) + +Estas keys deben existir en App Config para que `HealthService` funcione: + +| Key | Valor Esperado | +|---|---| +| `contentflow.common.COSMOS_DB_ENDPOINT` | `https://cosmos-{token}.documents.azure.com:443/` | +| `contentflow.common.COSMOS_DB_NAME` | `contentflow` | +| `contentflow.common.BLOB_STORAGE_ACCOUNT_NAME` | `st{token}` | +| `contentflow.common.BLOB_STORAGE_CONTAINER_NAME` | `content` | +| `contentflow.common.STORAGE_ACCOUNT_WORKER_QUEUE_URL` | `https://st{token}.queue.core.windows.net/` | +| `contentflow.common.STORAGE_WORKER_QUEUE_NAME` | `contentflow-execution-requests` | +| `contentflow.api.WORKER_ENGINE_API_ENDPOINT` | `https://worker-{token}.internal.{domain}` | + +#### Variables de Entorno del Container App + +| Container App | Variable | Valor | +|---|---|---| +| API | `AZURE_APP_CONFIG_ENDPOINT` | `https://appcs-{token}.azconfig.io` | +| API | `AZURE_CLIENT_ID` | Client ID de la Managed Identity | +| Worker | `AZURE_APP_CONFIG_ENDPOINT` | `https://appcs-{token}.azconfig.io` | +| Worker | `AZURE_CLIENT_ID` | Client ID de la Managed Identity | +| Web | `VITE_API_BASE_URL` | `https://api-{token}.internal.{domain}/api/` (o `/api/` si usa App Gateway) | + +### Gap Conocido: Queue Private DNS Zone + +El parámetro `existingQueuePrivateDnsZoneId` existe en `main.bicep` pero **no está mapeado** en `main.parameters.json`. Esto puede causar que el Private Endpoint de queue no tenga A record en la DNS Zone, haciendo que el health check de Storage Queue falle. Solución: agregar el mapeo al parameters file o configurar vía `azd env set`. + +--- + +## 12. Comandos de Verificación Manual + +### Desde el JumpBox: Verificar DNS de Todos los Servicios + +```bash +#!/bin/bash +# verify-health-dns.sh +# Ejecutar desde el JumpBox para verificar que todos los endpoints resuelven + +RESOURCE_TOKEN="r7b6gtdiob4em" +CAE_DOMAIN="delightfulwave-2b288efe.eastus2.azurecontainerapps.io" + +echo "=== Verificación DNS para Health Checks ===" + +echo "" +echo "[1/7] API Container App (Web → API)" +nslookup api-${RESOURCE_TOKEN}.internal.${CAE_DOMAIN} + +echo "" +echo "[2/7] Worker Container App (API → Worker)" +nslookup worker-${RESOURCE_TOKEN}.internal.${CAE_DOMAIN} + +echo "" +echo "[3/7] Web Container App (Navegador → Web)" +nslookup web-${RESOURCE_TOKEN}.internal.${CAE_DOMAIN} + +echo "" +echo "[4/7] App Configuration" +nslookup appcs-${RESOURCE_TOKEN}.azconfig.io + +echo "" +echo "[5/7] Cosmos DB" +nslookup cosmos-${RESOURCE_TOKEN}.documents.azure.com + +echo "" +echo "[6/7] Blob Storage" +STORAGE_NAME=$(echo "st${RESOURCE_TOKEN}" | cut -c1-24) +nslookup ${STORAGE_NAME}.blob.core.windows.net + +echo "" +echo "[7/7] Queue Storage" +nslookup ${STORAGE_NAME}.queue.core.windows.net + +echo "" +echo "=== Todos los endpoints deben resolver a IPs privadas (10.x.x.x) ===" +``` + +### Desde el JumpBox: Probar Health Check Directamente + +```bash +# Probar el endpoint de health del API +curl -sk https://api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/api/health/ | python3 -m json.tool + +# Probar un servicio específico +curl -sk https://api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/api/health/cosmos_db | python3 -m json.tool + +# Probar el worker directamente +curl -sk https://worker-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/status | python3 -m json.tool +``` + +### Verificar App Config Keys + +```bash +APP_CONFIG_NAME="appcs-r7b6gtdiob4em" + +# Listar todas las keys relevantes +az appconfig kv list \ + --name $APP_CONFIG_NAME \ + --key "contentflow.*" \ + --auth-mode login \ + --query "[].{key:key, value:value}" \ + -o table +``` + +### Verificar A Records en DNS Zones + +```bash +AILZ_RG="rg-mini-ailz" # Ajustar al nombre real + +DNS_ZONES=( + "privatelink.blob.core.windows.net" + "privatelink.queue.core.windows.net" + "privatelink.documents.azure.com" + "privatelink.azconfig.io" + "privatelink.azurecr.io" +) + +for ZONE in "${DNS_ZONES[@]}"; do + echo "=== $ZONE ===" + az network private-dns record-set a list -g $AILZ_RG -z "$ZONE" \ + --query "[].{name:name, ip:aRecords[0].ipv4Address}" -o table + echo "" +done +``` + +--- + +**Última actualización:** Marzo 2026 +**Aplica a:** ContentFlow v0.1.0, modo `ailz-integrated` diff --git a/Analysis/personal-private-configurations.md b/Analysis/personal-private-configurations.md new file mode 100644 index 0000000..4ec4a38 --- /dev/null +++ b/Analysis/personal-private-configurations.md @@ -0,0 +1,141 @@ +# Personal Private Configurations + +> **WARNING**: This file contains environment-specific configurations. Do NOT commit to a shared/public repository. + +--- + +## Variables + +Update these values to reuse all scripts below in any environment. + +```bash +# ── Environment ── +RESOURCE_GROUP="rg-contentflow-test-001" +SUBSCRIPTION_ID="d12d19a9-0636-4951-90a4-339158fd57d8" + +# ── Container App names ── +APP_API="api-vtkhgrhgdl4w2" +APP_WEB="web-vtkhgrhgdl4w2" +APP_WORKER="worker-vtkhgrhgdl4w2" +APPS=("$APP_API" "$APP_WEB" "$APP_WORKER") + +# ── Networking ── +MY_IP=$(curl -s ifconfig.me) +RULE_NAME="allow-my-ip" + +# ── Endpoints ── +API_URL="https://api-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" +WEB_URL="https://web-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" +WORKER_URL="https://worker-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" +``` + +--- + +## IP Access Restrictions on Azure Container Apps + +**Date Applied**: March 16, 2026 +**Resource Group**: `rg-contentflow-test-001` +**Subscription**: `ME-MngEnvMCAP887462-gereyeso-1` (`d12d19a9-0636-4951-90a4-339158fd57d8`) +**Region**: East US 2 + +### What was configured + +All three Container Apps were restricted to accept external ingress traffic **only** from a single IP address. Any request from a different IP receives a `403 Forbidden` response. + +| Container App | Allowed IP | Rule Name | +|---------------|-----------|-----------| +| `api-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | +| `web-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | +| `worker-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | + +### Service Endpoints (restricted) + +| Service | URL | +|---------|-----| +| Web UI | `$WEB_URL` | +| API | `$API_URL` | +| API Docs | `$API_URL/docs` | +| Worker | `$WORKER_URL` | + +### Why this is safe for internal communication + +Container Apps within the **same Container Apps Environment** communicate through the internal network. IP access restrictions apply only to **external** ingress traffic (from the internet). The service-to-service flow remains unaffected: + +``` +Browser (your IP) → Web UI → (browser fetches from) → API → (internal) → Worker +``` + +- **Web → API**: The React SPA runs in your browser, so API calls originate from your IP. +- **API → Worker**: Both are in the same Container Apps Environment; internal traffic bypasses IP restrictions. + +--- + +### Apply IP restriction to all apps + +```bash +for app in "${APPS[@]}"; do + echo "Restricting $app to $MY_IP ..." + az containerapp ingress access-restriction set \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --rule-name "$RULE_NAME" \ + --ip-address "$MY_IP/32" \ + --action Allow \ + --description "Allow only my IP" +done +``` + +### Update IP on all apps (when your IP changes) + +```bash +MY_IP=$(curl -s ifconfig.me) +echo "New IP: $MY_IP" + +for app in "${APPS[@]}"; do + echo "Updating $app ..." + az containerapp ingress access-restriction remove \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --rule-name "$RULE_NAME" + + az containerapp ingress access-restriction set \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --rule-name "$RULE_NAME" \ + --ip-address "$MY_IP/32" \ + --action Allow \ + --description "Allow only my IP" +done +``` + +### Remove all restrictions (make public again) + +```bash +for app in "${APPS[@]}"; do + echo "Removing restriction from $app ..." + az containerapp ingress access-restriction remove \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --rule-name "$RULE_NAME" +done +``` + +### Verify current restrictions + +```bash +for app in "${APPS[@]}"; do + echo "=== $app ===" + az containerapp ingress access-restriction list \ + -n "$app" \ + -g "$RESOURCE_GROUP" \ + --output table +done +``` + +### Quick health check + +```bash +for url in "$API_URL/health" "$WEB_URL" "$WORKER_URL"; do + echo "$url → $(curl -s -o /dev/null -w '%{http_code}' "$url")" +done +``` From 1913947ab053625d44d94912787ad38c2a3d7197 Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Mon, 30 Mar 2026 10:25:08 -0400 Subject: [PATCH 27/29] content_flow last tests --- .DS_Store | Bin 10244 -> 10244 bytes .gitignore | 2 ++ 2 files changed, 2 insertions(+) diff --git a/.DS_Store b/.DS_Store index 3a80f725a55157d3778b683937e2770d1c0c0f0e..ac1fbaace3b8e7d1fef68a8b0b7d21f1b0b144ef 100644 GIT binary patch delta 39 rcmZn(XbIS`PJpSUW%33A36?1#i#jKt5s-l}-U!Gs9@xw+_)`o3JF*Xv delta 39 vcmZn(XbIS`PJqe&@8k^v5-i6S3#m;$BOt@#@vl&5@*4p;Mu*MZf Date: Mon, 30 Mar 2026 10:44:04 -0400 Subject: [PATCH 28/29] content_flow cleaning branch --- .DS_Store | Bin 10244 -> 0 bytes Analysis/00-full-analysis.md | 1129 ----------- Analysis/01-overview.md | 430 ----- Analysis/02-architecture-detailed.md | 1920 ------------------- Analysis/03-use_cases_examples.md | 666 ------- Analysis/04-ai_lz_options.md | 1224 ------------ Analysis/05-mini-ailz.md | 1002 ---------- Analysis/06-service-health.md | 825 -------- Analysis/personal-private-configurations.md | 141 -- 9 files changed, 7337 deletions(-) delete mode 100644 .DS_Store delete mode 100644 Analysis/00-full-analysis.md delete mode 100644 Analysis/01-overview.md delete mode 100644 Analysis/02-architecture-detailed.md delete mode 100644 Analysis/03-use_cases_examples.md delete mode 100644 Analysis/04-ai_lz_options.md delete mode 100644 Analysis/05-mini-ailz.md delete mode 100644 Analysis/06-service-health.md delete mode 100644 Analysis/personal-private-configurations.md diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 8eabdfa9ec914e5c6f50a34b6412ae6fdbb9991e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHMO>7%Q6n>MRW=&eM`K3f4tol;5Y3q;#wL&GXn+g&@iQ-Ta(#GAjH_npvjH#4YP6&wu7vRd9*>yY{+i^I7U`E<~ z<9YAR``))ZGdp7dAjy^NAV3rV6ug*J58|yxVS08|m8PI&7$re}00#^!H<-%Me4)x( zHb@2}1CjyBfMh^2@NZxM*KAglc3;Z7WI!??8Q5Wf%@0w$m`nt+F*v$;7Wom2JzidA7$n!6M-!G(su{qy8{U#lVFEJcy#oO za&sUFU&^~=Kr&Fx0NcACfRmuX2CP)?-;ceqTc4+6W4c9kY*$XNEl$6A`|ESnHra+Y zg(dNv;5v4JGr>v7feD^?hCs|4>|A0;C&oYa&7B+RFL``HUx{5yFUBQnJZ@V#E-WDj zx)>ec+Yd4NE(;OE)sHtuuH51ns;nwV>RX9;0Di+@5J5Z?ba0W2EM6MeAxs^o*d$lO zO2l=)L;vjX7aUhj%L=x=EkpR!y9^)8a~08L5M{RN#Am$PLc~>i<=r8HFQK12tzfEe z6~f~$77CmL8(Feoopa`f*lK<079y&$5ixDQe4#7gs)k(!)n0ZXT&*PxiQ`H`JT4gE zftz(RmQJa~)~}F41WP8qd$3zC0-axbuS{O350SchHX=YW!C$lv{s?V^XCQ$dEFy&z zif{sf7r+#eLDL3XaT|DuSfBgz=qWN^OvQKff@Qj1JpO<*HMg`LXj9sh zcI9$vi563y=B>C%&6#JvX7Cl%%XLk^Xd1<|+H;=To@UyH!yU}5fhLz;HSH`dCh3Zu zb-8cKS11uBl2-dSH%|=4Pmc^lhvH|p2BMp1o{kShhlifo+KMQ>u~W}ZrYEQ_{Vm3dcs%C$g}*xhW2*Jo^M8<9vX@nWD-;5g2(6Oh97u?X+LHTVd&;U0Vs zzrr7+h4hgl0JD zTZH0|1Y>}qkVP^e8ITM}27CsZeGz5n{}<~2|1as13`hq4!wis?vD8=+6D)tI@6MI& z{G)hX!HX4USMnuy!5cGS?ff_%f3V|tzU#oYIMcC$^+X^`zQh(R|HPjGa{phPk^BGu H@BaTU0MB(q diff --git a/Analysis/00-full-analysis.md b/Analysis/00-full-analysis.md deleted file mode 100644 index 37e81e8..0000000 --- a/Analysis/00-full-analysis.md +++ /dev/null @@ -1,1129 +0,0 @@ -# ContentFlow — Complete Reference Analysis - -> **Single-file reference document for AI agents and developers.** -> This document consolidates all capabilities, architecture, prerequisites, out-of-scope boundaries, and example scenarios for the ContentFlow accelerator. Use it as the authoritative context source before working with this codebase. - ---- - -## Table of Contents - -1. [What is ContentFlow?](#1-what-is-contentflow) -2. [Core Concepts](#2-core-concepts) -3. [Full Capabilities](#3-full-capabilities) - - [3.1 Built-in Executors (37 total)](#31-built-in-executors-37-total) - - [3.2 Pipeline Engine Features](#32-pipeline-engine-features) - - [3.3 Web UI Features](#33-web-ui-features) - - [3.4 API Features](#34-api-features) - - [3.5 Worker Engine Features](#35-worker-engine-features) - - [3.6 Extensibility: Custom Executors](#36-extensibility-custom-executors) -4. [Prerequisites](#4-prerequisites) - - [4.1 Azure Services Required](#41-azure-services-required) - - [4.2 Developer Toolchain](#42-developer-toolchain) - - [4.3 Azure Permissions](#43-azure-permissions) - - [4.4 Service Quotas & Limits](#44-service-quotas--limits) -5. [Final Architecture](#5-final-architecture) - - [5.1 Component Overview](#51-component-overview) - - [5.2 Service Communication Map](#52-service-communication-map) - - [5.3 Data Flow](#53-data-flow) - - [5.4 Task Lifecycle](#54-task-lifecycle) - - [5.5 Cosmos DB Schema](#55-cosmos-db-schema) - - [5.6 Deployment Modes](#56-deployment-modes) - - [5.7 Security Model](#57-security-model) -6. [Out of Scope](#6-out-of-scope) -7. [Example Scenarios](#7-example-scenarios) - - [7.1 Invoice & Receipt Processing](#71-invoice--receipt-processing) - - [7.2 Enterprise Knowledge Base for RAG](#72-enterprise-knowledge-base-for-rag) - - [7.3 Contract Analysis](#73-contract-analysis) - - [7.4 Engineering PDF BOM Extraction](#74-engineering-pdf-bom-extraction) - - [7.5 Compliance & PII Detection](#75-compliance--pii-detection) - - [7.6 Web Content Ingestion](#76-web-content-ingestion) - - [7.7 Multilingual Document Translation](#77-multilingual-document-translation) - - [7.8 Email Attachment Triage](#78-email-attachment-triage) - - [7.9 Spreadsheet Data Pipeline](#79-spreadsheet-data-pipeline) - - [7.10 Knowledge Graph Construction](#710-knowledge-graph-construction) -8. [Included Sample Pipelines](#8-included-sample-pipelines) - ---- - -## 1. What is ContentFlow? - -**ContentFlow** is an open-source, enterprise-grade, cloud-native document and content processing accelerator built on Azure. It transforms unstructured content — PDFs, Word documents, Excel files, PowerPoint presentations, CSV files, web pages, images — into structured, intelligent, actionable data through orchestrated AI-powered pipelines. - -**Primary value proposition**: ContentFlow eliminates the infrastructure plumbing required to build scalable document processing systems on Azure. A developer can go from zero to a production-grade pipeline in hours rather than weeks by combining pre-built executors declaratively via YAML. - -**Repository**: [Azure/contentflow](https://github.com/Azure/contentflow) -**License**: MIT -**Runtime**: Python 3.12+ (backend), TypeScript/React 18 (frontend) - ---- - -## 2. Core Concepts - -### Pipeline -A **pipeline** is the fundamental unit of work. It defines an ordered sequence of executors that content passes through. Pipelines are declared in YAML and stored in Cosmos DB. - -```yaml -name: my-pipeline -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documents - - executor: azure_document_intelligence_extractor - settings: - doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" - - executor: recursive_text_chunker - settings: - chunk_size: 1000 - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - executor: ai_search_index_output - settings: - index_name: my-index -``` - -### Executor -An **executor** is a self-contained processing unit. Each executor receives a `Content` object, transforms or enriches it, and passes the result to the next step. Executors are composable, configurable via YAML settings, and can run in parallel. - -### Vault -A **vault** is a logical container that associates a document source with a pipeline. When enabled, the Worker service automatically discovers new documents arriving in the vault and queues them for processing. - -### Content Model -The standardized Python object (`Content`) that flows through every pipeline step: - -```python -Content - ├── id: ContentIdentifier # canonical_id, source_type, container, path, filename - ├── data: dict # All executor outputs accumulate here - ├── summary_data: dict # Execution metrics per executor - └── executor_logs: list # Per-step audit trail -``` - -All executor outputs are stored as named fields in `content.data`, enabling downstream executors to reference previous results via dot-notation paths (e.g., `doc_intell_output.tables`). - ---- - -## 3. Full Capabilities - -### 3.1 Built-in Executors (37 total) - -#### Input Executors (3) -| Executor ID | Description | -|---|---| -| `azure_blob_input_discovery` | Discovers files in Azure Blob Storage containers. Supports file extension filtering, prefix filtering, and incremental crawling via checkpoints (processes only new/modified blobs since last run). | -| `azure_blob_content_retriever` | Downloads blob content as bytes or saves to a temp file path for downstream processing. Supports parallel retrieval with configurable concurrency. | -| `content_retriever` | Loads content from local file system or arbitrary byte sources. Primarily used in local development and sample pipelines. | - -#### Document Extraction Executors (6) -| Executor ID | Description | -|---|---| -| `azure_document_intelligence_extractor` | Invokes Azure Document Intelligence (prebuilt-layout, prebuilt-document, prebuilt-read) to extract text, tables, key-value pairs, and figures from any document. Supports markdown or plain text output format, page range selection, locale hints, and additional features like `ocrHighResolution`. | -| `azure_content_understanding_extractor` | Uses Azure Content Understanding for advanced document analysis including prebuilt invoice, receipt, business card, and ID models. | -| `pdf_extractor` | Extracts text, page-level chunks, and embedded images from PDF files using PyMuPDF. No Azure service dependency — runs locally. | -| `word_extractor` | Extracts text, headings, tables, and metadata from `.docx` files using python-docx. | -| `powerpoint_extractor` | Extracts slide content, speaker notes, embedded images, and metadata from `.pptx` files using python-pptx. | -| `excel_extractor` | Extracts sheets, tables, cell values, formulas, images, and metadata from `.xlsx`/`.xlsm` files using openpyxl. Supports first-row-as-header detection, sheet filtering, and table range detection. | -| `csv_extractor` | Parses CSV files with configurable delimiter, quoting, and encoding. | - -#### AI Processing Executors (8) -| Executor ID | Description | -|---|---| -| `azure_openai_agent` | Sends content to Azure OpenAI (GPT-4.1, GPT-4.1-mini, etc.) with a configurable system prompt and user template. The most flexible executor — suitable for summarization, extraction, classification, transformation, and any custom reasoning task. | -| `azure_openai_embeddings` | Generates vector embeddings using Azure OpenAI embedding models (text-embedding-3-small, text-embedding-3-large). Output format compatible with Azure AI Search vector fields. | -| `text_summarizer` | Generates concise summaries with configurable max length and style. Built on `azure_openai_agent`. | -| `entity_extractor` | Extracts named entities (persons, organizations, dates, amounts, locations) from text. Configurable entity types. Built on `azure_openai_agent`. | -| `sentiment_analyser` | Document-level, sentence-level, or aspect-based sentiment analysis with confidence scores. Supports 3-point and 5-point scales and emotion detection. | -| `content_classifier` | Multi-class and multi-label classification with confidence scores and explanations. Accepts custom category lists and descriptions. | -| `pii_detector` | Detects and optionally redacts Personally Identifiable Information (PII) such as names, emails, phone numbers, SSNs, credit cards, addresses. | -| `keyword_extractor` | Extracts top-N keywords and key phrases from text content. | - -#### Transformation Executors (6) -| Executor ID | Description | -|---|---| -| `recursive_text_chunker` | Splits long text into overlapping chunks using recursive character splitting with configurable chunk size, overlap, and separators. | -| `table_row_splitter` | Splits tabular data (list-of-lists, list-of-dicts, CSV string, Word tables, Excel rows) into individual `Content` objects — one per row — enabling per-row parallel processing downstream. | -| `field_mapper` | Renames, remaps, and transforms fields within `content.data` using a declarative mapping configuration. | -| `field_selector` | Includes or excludes specific fields from `content.data` to control what data is passed forward. | -| `language_detector` | Detects the primary language of text content. | -| `content_translator` | Translates content to one or more target languages using Azure OpenAI. | - -#### Output Executors (3) -| Executor ID | Description | -|---|---| -| `azure_blob_output` | Writes processed content (JSON, text, binary) back to Azure Blob Storage. Supports custom path templates and content serialization. | -| `ai_search_index_output` | Indexes processed content into Azure AI Search. Supports vector fields, semantic configuration, and batch indexing. | -| `gptrag_search_index_document_generator` | Generates documents in GPT-RAG-compatible format for Azure AI Search, including chunked content and embeddings for RAG scenarios. | - -#### Control Flow Executors (5) -| Executor ID | Description | -|---|---| -| `subpipeline` | Executes another pipeline as a nested sub-workflow. Enables pipeline composition and reuse. | -| `for_each_content` | Iterates over a list of content items and applies a sub-pipeline to each one. | -| `fan_in_aggregator` | Collects and merges results from parallel branches back into a single content stream. | -| `document_set_initializer` | Initializes a multi-document analysis session for cross-document operations. | -| `document_set_collector` | Collects documents into a set for batch cross-document processing. | - -#### Cross-Document Executors (3) -| Executor ID | Description | -|---|---| -| `cross_document_comparison` | Compares multiple documents side-by-side using AI to identify similarities, differences, and patterns. | -| `cross_document_field_aggregator` | Aggregates specific fields across a document set (e.g., collect all "summary" fields from 50 documents). | -| `cosmos_db_lookup` | Looks up metadata or previously stored results from Cosmos DB during pipeline execution. Useful for enrichment and deduplication. | - -#### Utility Executors (3) -| Executor ID | Description | -|---|---| -| `pass_through` | No-op executor. Useful for pipeline testing and debugging. | -| `web_scraper` | Crawls web pages using Playwright (headless Chromium). Supports CSS selectors, depth control, and JavaScript-rendered pages. | -| `cosmos_db_lookup` | Retrieves enrichment data from Cosmos DB based on a key field in the content. | - ---- - -### 3.2 Pipeline Engine Features - -- **Declarative YAML definition** — pipelines are stored as YAML and parsed at runtime -- **DAG execution** — pipelines can define explicit `execution_sequence` for dependency control -- **Async/await throughout** — all executors are async; built on Python asyncio -- **Parallel execution** — `ParallelExecutor` base class enables `max_concurrent` processing (default: 3 concurrent items) -- **Conditional execution** — any executor supports a `condition` setting (Python expression evaluated against `content.data`) -- **Error handling** — `fail_pipeline_on_error` and `continue_on_error` per executor; up to 3 retries with configurable delay -- **Timeout control** — per-executor `timeout_secs` setting -- **Debug mode** — verbose logging per executor via `debug_mode: true` -- **Nested fields** — all settings support dot-notation for accessing nested data (e.g., `doc_intell_output.tables`) -- **Environment variable substitution** — any setting value can reference env vars with `${VAR_NAME}` syntax -- **Sub-pipelines** — pipelines can invoke other pipelines as steps, enabling workflow composition -- **Parallel fan-out/fan-in** — native support for splitting content into parallel branches and merging results -- **Incremental processing** — vault crawl checkpoints prevent duplicate reprocessing - ---- - -### 3.3 Web UI Features - -**Technology**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn/ui, Radix UI, ReactFlow, Monaco Editor - -| Feature | Description | -|---|---| -| **Visual Pipeline Builder** | Drag-and-drop graphical interface built on ReactFlow. Nodes represent executors; edges represent data flow. | -| **YAML Editor** | Monaco-powered code editor with syntax highlighting for editing pipeline definitions directly. | -| **Template Gallery** | Pre-built pipeline templates for common scenarios, ready to clone and customize. | -| **Vault Management** | Create, edit, enable/disable document vaults. Associate vaults with pipelines. | -| **Execution Dashboard** | Real-time view of pipeline execution status, metrics, and per-executor output. | -| **Executor Catalog Browser** | Browse all 37 executors with descriptions, settings schemas, and usage examples. | -| **Knowledge Graph Viewer** | Visualization of entity-relationship graphs produced by knowledge graph pipelines. | - -**Routes**: -- `/` — Home dashboard -- `/?view=pipeline` — Pipeline builder -- `/?view=vaults` — Vault management -- `/?view=graph` — Knowledge graph visualization -- `/templates` — Template gallery - ---- - -### 3.4 API Features - -**Technology**: FastAPI 0.128, Python 3.12+, Uvicorn, Pydantic v2, Azure SDK -**Port**: 8090 -**Authentication**: Azure Managed Identity (no API keys in production) - -| Endpoint group | Operations | -|---|---| -| `GET /api/health` | System health check — verifies Cosmos DB, Blob, Queue, App Config connectivity | -| `GET/POST/PUT/DELETE /api/pipelines` | Full CRUD for pipeline definitions; trigger manual execution | -| `GET/POST/PUT/DELETE /api/vaults` | Vault management including enable/disable | -| `GET /api/vaults/{id}/executions` | Vault execution history and status | -| `GET /api/executors` | Browse executor catalog with full settings schemas | -| `GET /docs` | Interactive Swagger UI | -| `GET /redoc` | ReDoc documentation | - ---- - -### 3.5 Worker Engine Features - -**Technology**: Python 3.12+, multiprocessing, Azure Storage Queue -**Port**: 8099 (health API) - -| Feature | Description | -|---|---| -| **Multi-process architecture** | N Source Workers + M Processing Workers run as independent OS processes | -| **Source Workers** | Poll enabled vaults every 5 minutes (configurable), discover new content, enqueue tasks | -| **Processing Workers** | Poll Azure Storage Queue, execute full pipelines on content items | -| **Incremental crawling** | Checkpoints stored in Cosmos DB prevent reprocessing already-seen documents | -| **Distributed locking** | Lock TTL prevents duplicate processing when multiple workers poll simultaneously | -| **Auto-restart** | Monitor loop detects crashed worker processes and replaces them automatically | -| **Graceful shutdown** | SIGINT/SIGTERM handled via shared `multiprocessing.Event`; workers finish current task before stopping | -| **Configurable parallelism** | `NUM_PROCESSING_WORKERS` and `NUM_SOURCE_WORKERS` env vars | -| **Task retries** | Up to 3 retries per task with configurable delay | - ---- - -### 3.6 Extensibility: Custom Executors - -ContentFlow is designed for extension. New executors can be created by subclassing one of three base classes: - -| Base Class | Use case | -|---|---| -| `BaseExecutor` | Single content item or list processing; implement `process_input()` | -| `ParallelExecutor` | Automatic parallel processing; implement `process_content_item()` once; framework handles concurrency | -| `InputExecutor` | Input source discovery; implement `crawl()` to yield new content items | - -After implementing the class: -1. Register it in `executor_catalog.yaml` with its `id`, `module_path`, `class_name`, tags, and `settings_schema` -2. Import it in `contentflow/executors/__init__.py` - -Custom executors work identically to built-in ones — they appear in the Web UI, accept YAML configuration, support all base class features (debug mode, conditions, retries, timeouts), and integrate with the full pipeline engine. - ---- - -## 4. Prerequisites - -### 4.1 Azure Services Required - -| Service | SKU (Basic mode) | SKU (AILZ mode) | Purpose | -|---|---|---|---| -| **Azure Container Apps Environment** | Consumption | Consumption (Internal) | Hosts API, Worker, Web containers | -| **Azure Cosmos DB** | Serverless | Serverless + Private Endpoint | Pipeline definitions, execution state, vault metadata | -| **Azure Blob Storage** | Standard LRS, Hot | Standard LRS + Private Endpoint | Document storage, processed outputs | -| **Azure Storage Queue** | Standard | Standard + Private Endpoint | Task queue between Source and Processing Workers | -| **Azure App Configuration** | Standard | Standard + Private Endpoint | Centralized runtime configuration | -| **Azure Application Insights** | Pay-per-use | Pay-per-use | Distributed tracing, monitoring | -| **Azure Log Analytics** | PerGB2018 | PerGB2018 (shared) | Log aggregation | -| **Azure Container Registry** | Standard | **Premium** (required for Private Endpoint) | Container image registry | -| **Azure Managed Identity** | User-assigned | User-assigned | Passwordless authentication between all services | - -**Optional Azure services** (required for specific executors): - -| Service | Required by executor(s) | -|---|---| -| **Azure AI Foundry / OpenAI** | `azure_openai_agent`, `azure_openai_embeddings`, `text_summarizer`, `entity_extractor`, `sentiment_analyser`, `content_classifier`, `pii_detector`, `keyword_extractor`, `content_translator` | -| **Azure Document Intelligence** | `azure_document_intelligence_extractor` | -| **Azure Content Understanding** | `azure_content_understanding_extractor` | -| **Azure AI Search** | `ai_search_index_output`, `gptrag_search_index_document_generator` | - -### 4.2 Developer Toolchain - -```bash -# Required for local development -python --version # 3.12+ -node --version # 18+ -npm --version # 9+ -docker --version # Any recent version -az --version # Azure CLI 2.60+ -azd version # Azure Developer CLI 1.5+ - -# Required for deployment -git --version # Any recent version -``` - -**Python dependencies** (installed from `requirements.txt` per service): -- `fastapi>=0.128.0`, `uvicorn>=0.40.0`, `pydantic>=2.12` -- `azure-identity>=1.25.1`, `azure-cosmos>=4.14.3` -- `azure-storage-blob>=12.27.1`, `azure-storage-queue>=12.14.1` -- `azure-appconfiguration-provider>=2.3.1` -- `azure-ai-documentintelligence` (for Document Intelligence) -- `openai` (for Azure OpenAI) -- `pymupdf` (for PDF extraction) -- `python-docx`, `python-pptx`, `openpyxl` (Office documents) -- `playwright` (for web scraping — requires browser install) -- `agent-framework` (Microsoft Agent Framework — pipeline orchestration) - -### 4.3 Azure Permissions - -| Permission | Scope | Required for | -|---|---|---| -| `Contributor` | ContentFlow Resource Group | Deploy all resources | -| `User Access Administrator` | ContentFlow Resource Group | Assign RBAC to Managed Identity | -| `Network Contributor` | VNet Resource Group | Create Private Endpoints (AILZ mode only) | -| `Private DNS Zone Contributor` | DNS Zone Resource Group | Register PE records (AILZ mode only) | - -**RBAC roles assigned automatically to the Managed Identity during deployment**: -- `Storage Blob Data Contributor` on Storage Account -- `Storage Queue Data Contributor` on Storage Account -- `Cosmos DB Built-in Data Contributor` on Cosmos DB -- `App Configuration Data Reader` on App Configuration -- `AcrPull` on Container Registry -- `Cognitive Services User` on AI Services (when applicable) - -### 4.4 Service Quotas & Limits - -| Limit | Default | Notes | -|---|---|---| -| Container Apps replicas | 1–2 per service | Auto-scales based on load | -| Worker processes | Configurable | `NUM_PROCESSING_WORKERS` env var | -| Queue message visibility timeout | 300 seconds | Task must complete within 5 minutes or is retried | -| Task max retries | 3 | Configurable via pipeline settings | -| Task timeout | 600 seconds | 10 minutes per pipeline execution | -| Cosmos DB throughput | Serverless (on-demand) | No pre-provisioned RU/s needed | -| Azure OpenAI TPM | Varies by deployment | Should be sized for expected concurrency | - ---- - -## 5. Final Architecture - -### 5.1 Component Overview - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ AZURE CONTAINER APPS ENVIRONMENT │ -│ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ │ -│ │ contentflow- │ │ contentflow- │ │ contentflow- │ │ -│ │ web │ │ api │ │ worker │ │ -│ │ │ │ │ │ │ │ -│ │ React 18 + │──▶│ FastAPI │ │ Multi-process │ │ -│ │ TypeScript │ │ Python 3.12+ │ │ Python 3.12+ │ │ -│ │ Port: 80 │ │ Port: 8090 │ │ Port: 8099 │ │ -│ │ │ │ │ │ (health only) │ │ -│ │ Pipeline │ │ Routers: │ │ │ │ -│ │ Builder UI │ │ /pipelines │ │ Source Workers │ │ -│ │ Vault Manager │ │ /vaults │ │ (crawl vaults) │ │ -│ │ Exec Dashboard │ │ /executors │ │ │ │ -│ │ Template Gall. │ │ /health │ │ Processing │ │ -│ └─────────────────┘ └────────┬────────┘ │ Workers │ │ -│ │ │ (run pipelines) │ │ -└──────────────────────────────────┼────────────┴──────────────────┘ │ - │ │ - ┌─────────────────────────┼────────────────────┼──────────────┐ - │ ▼ ▼ │ - │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ - │ │ Cosmos DB │ │ Blob Storage │ │ Storage Queue │ │ - │ │ (Serverless)│ │ (LRS, Hot) │ │ (processing │ │ - │ │ │ │ │ │ tasks) │ │ - │ │ pipelines │ │ vaults/ │ │ │ │ - │ │ vaults │ │ processed/ │ │ │ │ - │ │ executions │ │ outputs/ │ │ │ │ - │ │ checkpoints │ │ │ │ │ │ - │ └──────────────┘ └──────────────┘ └─────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ - │ │ App Config │ │ App Insights│ │ Container │ │ - │ │ (Standard) │ │ + Log Anal. │ │ Registry │ │ - │ └──────────────┘ └──────────────┘ └─────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │ - │ │ AI Foundry │ │ Document │ │ Azure AI │ │ - │ │ (OpenAI) │ │ Intelligence│ │ Search │ │ - │ │ (optional) │ │ (optional) │ │ (optional) │ │ - │ └──────────────┘ └──────────────┘ └─────────────────┘ │ - │ AZURE MANAGED SERVICES │ - └──────────────────────────────────────────────────────────────┘ -``` - -### 5.2 Service Communication Map - -| From | To | Protocol | Auth | -|---|---|---|---| -| Web UI | ContentFlow API | HTTPS REST | None (same environment) | -| API | Cosmos DB | Azure SDK over HTTPS | Managed Identity | -| API | Blob Storage | Azure SDK over HTTPS | Managed Identity | -| API | Azure App Configuration | Azure SDK over HTTPS | Managed Identity | -| Worker (Source) | Cosmos DB | Azure SDK over HTTPS | Managed Identity | -| Worker (Source) | Storage Queue | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Storage Queue | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Cosmos DB | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Blob Storage | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Azure OpenAI | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Document Intelligence | Azure SDK over HTTPS | Managed Identity | -| Worker (Processing) | Azure AI Search | Azure SDK over HTTPS | Managed Identity | - -### 5.3 Data Flow - -``` -1. CONTENT DISCOVERY - Source Worker - │ - ├── Query Cosmos DB → enabled vaults - ├── For each vault → read associated pipeline - ├── Run input executor (e.g., azure_blob_input_discovery) - ├── Discover new files since last checkpoint - ├── Create ContentProcessingTask per file - ├── Enqueue tasks → Azure Storage Queue - └── Save new crawl checkpoint → Cosmos DB - -2. CONTENT PROCESSING - Processing Worker - │ - ├── Poll Storage Queue (up to 32 messages) - ├── Deserialize ContentProcessingTask - ├── Acquire distributed lock (prevent duplicate processing) - ├── Load pipeline YAML from Cosmos DB - ├── For each pipeline step (skipping input executors): - │ ├── Instantiate executor - │ ├── Check condition (if set) - │ ├── Call executor.process_input(content) - │ └── Accumulate results in content.data - ├── Update execution status → Cosmos DB (COMPLETED / FAILED) - ├── Write output → Blob Storage (if configured) - └── Delete message from queue - -3. OUTPUT - Configured output executors write to: - ├── Azure Blob Storage (JSON, text, binary) - ├── Azure AI Search (indexed documents) - └── Any custom destination via custom executor -``` - -### 5.4 Task Lifecycle - -``` -[PENDING] ← Task created and enqueued - │ - ▼ -[RUNNING] ← Processing Worker picks up the task - │ - ├──── Success ──→ [COMPLETED] - │ │ - │ Store results in Cosmos DB + Blob - │ - ├──── Failure ──→ [FAILED] - │ │ - │ ├── retry_count < max_retries (3)? - │ │ └── Yes → re-enqueue → [PENDING] - │ └── No → [FAILED] (permanent) - │ - └──── Cancelled ─→ [CANCELLED] -``` - -### 5.5 Cosmos DB Schema - -**Database**: `contentflow-db` - -| Container | Partition Key | Contents | -|---|---|---| -| `pipelines` | `/id` | Pipeline YAML definitions, graph nodes/edges, metadata | -| `vaults` | `/id` | Vault configurations, associated pipeline IDs | -| `vault_executions` | `/vault_id` | Per-document execution records with status and outputs | -| `pipeline_executions` | `/pipeline_id` | Direct pipeline execution records | -| `vault_crawl_checkpoints` | `/vault_id` | Last crawl timestamp per vault+executor | -| `executor_locks` | `/task_id` | Distributed locks for deduplication | -| `executor_catalog` | `/id` | Cached executor catalog (loaded from YAML at startup) | - -### 5.6 Deployment Modes - -#### Basic Mode (Public Endpoints) -All Azure resources expose public endpoints. Suitable for development, demos, and non-regulated environments. - -``` -Internet → Container Apps (external ingress) → Azure Services (public endpoints) -``` - -**Deploy**: `azd up` (one command) - -#### AILZ-Integrated Mode (Private Endpoints) -All Azure resources have `publicNetworkAccess: Disabled`. Traffic flows only through Private Endpoints inside a VNet. Suitable for enterprise, regulated environments, and AI Landing Zone integration. - -``` -JumpBox/VPN → VNet → Container Apps (internal LB) → Azure Services (Private Endpoints) -``` - -**Differences from Basic**: -- Container Apps Environment: `internal: true` (no public ingress) -- All services: `publicNetworkAccess: Disabled` -- Network ACLs: `defaultAction: Deny`, `bypass: AzureServices` -- Container Registry: must be **Premium** SKU (required for Private Endpoint support) -- Requires pre-existing VNet with subnets: `pe-subnet` and `aca-env-subnet` -- Requires 6 Private DNS Zones linked to the VNet: - - `privatelink.blob.core.windows.net` - - `privatelink.queue.core.windows.net` - - `privatelink.documents.azure.com` - - `privatelink.azconfig.io` - - `privatelink.azurecr.io` - - `privatelink.cognitiveservices.azure.com` - -**Deploy**: `azd up` with `ailzMode=ailz-integrated` parameter and pre-existing network resource IDs. - -### 5.7 Security Model - -| Principle | Implementation | -|---|---| -| **Zero-Trust** | No passwords, no connection strings. All service-to-service communication uses Managed Identity. | -| **Passwordless** | `ChainedTokenCredential` (Managed Identity → `DefaultAzureCredential`) used across all Azure SDK calls. | -| **Least Privilege** | Each service's Managed Identity is assigned only the RBAC roles it needs on each resource. | -| **Network Isolation** | AILZ mode enforces private networking; Basic mode exposes public endpoints with HTTPS. | -| **Secrets Management** | All secrets are either environment variables injected at deployment time or resolved from Azure App Configuration. No secrets in code or Git. | -| **Centralized Config** | Azure App Configuration is the single source of truth for runtime settings; local `.env` is for development only. | -| **CORS** | API configures `allow_origins: ["*"]` (suitable for internal deployments; restrict in production). | - ---- - -## 6. Out of Scope - -The following capabilities are **not included** in ContentFlow out-of-the-box and would require custom executor development, additional Azure services, or integration work: - -| Capability | Notes | -|---|---| -| **SharePoint read/write connector** | `ContentIdentifier.source_type` mentions "sharepoint" but no executor or connector exists. Would require Microsoft Graph API integration as a custom executor. | -| **OneDrive connector** | Same as SharePoint — no native executor. The Web UI shows it as a planned input type. | -| **Excel/XLSX writer** | `ExcelExtractorExecutor` reads Excel but there is no executor to **write** formatted `.xlsx` files (e.g., with conditional formatting). Requires custom executor using openpyxl. | -| **Confidence score per BOM cell (Document Intelligence)** | The `extract_tables()` method in `DocumentIntelligenceConnector` returns cell content and position but does not expose `cell.confidence` from the SDK response. Requires a code extension to the connector. | -| **Email (Exchange/Outlook) connector** | No native email source executor. Would require Microsoft Graph API integration. | -| **Relational database connector** (SQL, PostgreSQL) | No native DB input executor. Would require a custom `InputExecutor` subclass. | -| **Real-time / streaming processing** | ContentFlow is a batch/async pipeline system. It does not support real-time streaming ingestion (e.g., Kafka, Event Hub consumer). | -| **Chat interface** | No conversational UI or chat bot. ContentFlow processes documents, it does not serve a chat endpoint. Integration with Copilot Studio or Azure Bot Service is out of scope. | -| **Azure AI Search index schema management** | The `ai_search_index_output` executor writes to an existing index but does not create or manage index schemas. Index schema provisioning must happen separately. | -| **Custom Document Intelligence model training** | ContentFlow uses pre-built or previously trained models. Training custom Document Intelligence models is outside the accelerator's scope. | -| **Dynamics 365 / SAP integration** | No native connectors for line-of-business systems. Outputs to Blob or AI Search can be consumed by downstream integrations. | -| **Multi-tenant isolation** | All pipelines and vaults within a deployment share the same Cosmos DB and Blob Storage. There is no tenant-level data separation built in. | -| **Role-based access control in the UI** | The Web UI has no authentication or authorization layer. It is intended for internal use by platform operators. | -| **Built-in pipeline scheduling** | Vaults trigger continuous background polling; there is no cron-style scheduler. Time-based triggers must be implemented externally (e.g., Logic Apps, Azure Functions). | -| **Video or audio processing** | No native executors for audio transcription or video analysis. These would require Azure Speech or Azure Video Indexer custom executors. | - ---- - -## 7. Example Scenarios - -### 7.1 Invoice & Receipt Processing - -**Goal**: Extract structured data from incoming invoices PDFs and images; classify by document type; flag anomalies; output to Blob Storage. - -**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_content_understanding_extractor` (prebuilt-invoice) → `content_classifier` → `field_mapper` → `azure_blob_output` - -```yaml -name: invoice-processing -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: invoices-incoming - file_extensions: [".pdf", ".png", ".jpg"] - - - executor: azure_blob_content_retriever - - - executor: azure_content_understanding_extractor - settings: - model_id: prebuilt-invoice - output_field: invoice_data - - - executor: content_classifier - settings: - categories: ["invoice", "receipt", "purchase_order", "credit_note"] - include_confidence: true - - - executor: field_mapper - settings: - mappings: - invoice_data.VendorName: vendor_name - invoice_data.InvoiceTotal.amount: total_amount - invoice_data.InvoiceDate: invoice_date - - - executor: azure_blob_output - settings: - blob_container_name: invoices-processed - output_format: json -``` - ---- - -### 7.2 Enterprise Knowledge Base for RAG - -**Goal**: Process a mixed-format document library (PDF, Word, Excel), chunk text, generate embeddings, and index in Azure AI Search to power a RAG chatbot. - -**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_document_intelligence_extractor` → `recursive_text_chunker` → `azure_openai_embeddings` → `gptrag_search_index_document_generator` → `ai_search_index_output` - -```yaml -name: knowledge-base-ingestion -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: company-documents - file_extensions: [".pdf", ".docx", ".xlsx"] - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - settings: - doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" - model_id: prebuilt-layout - extract_text: true - extract_tables: true - - - executor: recursive_text_chunker - settings: - chunk_size: 800 - chunk_overlap: 150 - input_field: doc_intell_output.text - - - executor: azure_openai_embeddings - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: text-embedding-3-small - input_field: chunks - - - executor: gptrag_search_index_document_generator - settings: - index_name: company-knowledge-base - - - executor: ai_search_index_output - settings: - endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" - index_name: company-knowledge-base -``` - ---- - -### 7.3 Contract Analysis - -**Goal**: Extract obligations, parties, dates, monetary values, and risk clauses from legal contracts. Generate an executive summary per document. - -**Key executors**: `pdf_extractor` → `entity_extractor` → `azure_openai_agent` → `text_summarizer` → `azure_blob_output` - -```yaml -name: contract-analysis -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: contracts - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - settings: - extract_text: true - extract_pages: true - - - executor: entity_extractor - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - entity_types: ["person", "organization", "date", "monetary_amount", "jurisdiction"] - output_field: entities - - - executor: azure_openai_agent - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - instructions: | - You are a legal contract analyst. Analyze the contract and extract: - 1. Key obligations per party - 2. Payment terms and amounts - 3. Termination clauses - 4. Governing law and jurisdiction - 5. Risk flags (unusual or one-sided clauses) - Respond in structured JSON. - input_field: pdf_output.text - output_field: contract_analysis - - - executor: text_summarizer - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - max_length: 400 - style: executive summary for legal review - input_field: pdf_output.text - output_field: summary - - - executor: azure_blob_output - settings: - blob_container_name: contracts-analyzed -``` - ---- - -### 7.4 Engineering PDF BOM Extraction - -**Goal**: Extract Bill of Materials (BOM) tables from engineering PDFs using Azure Document Intelligence, flag low-confidence line items (<90%), and write output to Azure Blob Storage. *(See fit assessment in previous analysis for implementation gap details.)* - -**Key executors**: `azure_blob_input_discovery` → `azure_blob_content_retriever` → `azure_document_intelligence_extractor` → `table_row_splitter` → `field_mapper` → [custom: `bom_excel_writer`] → [custom: `sharepoint_output`] - -```yaml -name: engineering-bom-extraction -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: engineering-pdfs - file_extensions: [".pdf"] - - - executor: azure_blob_content_retriever - settings: - use_temp_file_for_content: true - - - executor: azure_document_intelligence_extractor - settings: - doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" - model_id: prebuilt-layout - extract_tables: true - extract_text: false - output_field: doc_intell_output - - - executor: table_row_splitter - settings: - table_field: doc_intell_output.tables.0 - table_format: auto - has_header: true - skip_empty_rows: true - include_row_index: true - - - executor: field_mapper - settings: - mappings: - row_data.Item: item_number - row_data.Description: description - row_data.Qty: quantity - row_data.Unit: unit - row_data.Part_Number: part_number - - # Steps 6 and 7 require custom executors (not built-in): - # - bom_excel_writer: writes .xlsx with red fill on rows where confidence < 0.90 - # - sharepoint_output: writes file back to SharePoint via Microsoft Graph API - - - executor: azure_blob_output - settings: - blob_container_name: bom-output - output_format: json -``` - -**Custom executor gaps required for full Phase 1**: -- `bom_excel_writer` — writes formatted Excel with `openpyxl`, applies `PatternFill(red)` to rows where confidence < 0.90 -- `sharepoint_output` — uploads file to SharePoint document library via Microsoft Graph API -- Extend `DocumentIntelligenceConnector.extract_tables()` to expose `cell.confidence` from the SDK response - ---- - -### 7.5 Compliance & PII Detection - -**Goal**: Scan documents for PII, classify by sensitivity level, and generate compliance risk reports. - -**Key executors**: `pdf_extractor` / `word_extractor` → `pii_detector` → `content_classifier` → `azure_openai_agent` → `azure_blob_output` - -```yaml -name: compliance-scan -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documents-review - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - settings: - extract_text: true - - - executor: pii_detector - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - pii_types: ["name", "email", "phone", "ssn", "credit_card", "address", "dob"] - redact: false - output_field: pii_results - - - executor: content_classifier - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - categories: ["public", "internal", "confidential", "restricted"] - include_confidence: true - output_field: classification - - - executor: azure_openai_agent - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - instructions: | - Based on PII findings and document classification, assess regulatory exposure: - - GDPR, HIPAA, PCI-DSS applicability - - Risk level: low / medium / high / critical - - Required remediation actions - output_field: compliance_report - - - executor: azure_blob_output - settings: - blob_container_name: compliance-reports -``` - ---- - -### 7.6 Web Content Ingestion - -**Goal**: Crawl product documentation or support sites, chunk content, generate embeddings, and index in Azure AI Search for a RAG chatbot. - -**Key executors**: `web_scraper` → `recursive_text_chunker` → `language_detector` → `azure_openai_embeddings` → `ai_search_index_output` - -```yaml -name: web-content-ingestion -steps: - - executor: web_scraper - settings: - urls: - - "https://docs.company.com" - - "https://support.company.com" - max_depth: 3 - selectors: ["article", "main", ".content"] - output_field: scraped_content - - - executor: recursive_text_chunker - settings: - chunk_size: 1500 - chunk_overlap: 200 - input_field: scraped_content.text - - - executor: language_detector - settings: - input_field: scraped_content.text - output_field: detected_language - - - executor: azure_openai_embeddings - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: text-embedding-3-small - - - executor: ai_search_index_output - settings: - endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" - index_name: web-content-rag -``` - ---- - -### 7.7 Multilingual Document Translation - -**Goal**: Detect language of incoming documents, translate to target languages, and index translated content. - -**Key executors**: `pdf_extractor` → `language_detector` → `recursive_text_chunker` → `content_translator` → `azure_openai_embeddings` → `ai_search_index_output` - -```yaml -name: multilingual-translation -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: original-content - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - settings: - extract_text: true - - - executor: language_detector - settings: - input_field: pdf_output.text - output_field: source_language - - - executor: recursive_text_chunker - settings: - chunk_size: 2000 - input_field: pdf_output.text - - - executor: content_translator - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - target_languages: ["en", "es", "fr", "de", "pt"] - output_field: translations - - - executor: azure_openai_embeddings - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: text-embedding-3-small - - - executor: ai_search_index_output - settings: - endpoint: "${AZURE_AI_SEARCH_ENDPOINT}" - index_name: multilingual-content -``` - ---- - -### 7.8 Email Attachment Triage - -**Goal**: Process email attachments, detect sentiment, extract action items and deadlines, classify by category, and route to the appropriate team queue. - -**Key executors**: `pdf_extractor` → `sentiment_analyser` → `entity_extractor` → `azure_openai_agent` → `content_classifier` → `azure_blob_output` - -```yaml -name: email-triage -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: email-attachments - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - settings: - extract_text: true - - - executor: sentiment_analyser - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - granularity: document - include_confidence: true - output_field: sentiment - - - executor: entity_extractor - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - entity_types: ["person", "date", "organization", "action_item"] - output_field: entities - - - executor: azure_openai_agent - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - instructions: | - Extract from this email: - 1. Action items and who owns them - 2. Deadlines mentioned - 3. Urgency level: low / medium / high / critical - 4. Subject matter summary in one sentence - output_field: triage_result - - - executor: content_classifier - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1-mini - categories: ["technical_support", "billing", "complaint", "legal", "general_inquiry"] - output_field: category - - - executor: azure_blob_output - settings: - blob_container_name: email-triaged -``` - ---- - -### 7.9 Spreadsheet Data Pipeline - -**Goal**: Process Excel files row by row, normalize field names, validate data, and export cleaned records to Blob Storage as JSON. - -**Key executors**: `content_retriever` → `excel_extractor` → `table_row_splitter` → `field_mapper` → `field_selector` → `azure_blob_output` - -```yaml -name: spreadsheet-pipeline -steps: - - executor: content_retriever - settings: - use_temp_file_for_content: true - - - executor: excel_extractor - settings: - extract_text: false - extract_sheets: true - extract_tables: true - first_row_as_header: true - skip_hidden_sheets: true - output_field: excel_output - - - executor: table_row_splitter - settings: - table_field: excel_output.sheets.0.table - table_format: auto - has_header: true - skip_empty_rows: true - include_row_index: true - - - executor: field_mapper - settings: - mappings: - row_data.CustomerID: customer_id - row_data.CustomerName: name - row_data.EmailAddress: email - row_data.PurchaseAmount: amount - - - executor: field_selector - settings: - include_fields: ["customer_id", "name", "email", "amount", "row_index"] - - - executor: azure_blob_output - settings: - blob_container_name: processed-data - output_format: json -``` - ---- - -### 7.10 Knowledge Graph Construction - -**Goal**: Extract entities and relationships from a corporate document corpus and build a structured knowledge graph for organizational intelligence. - -**Key executors**: `azure_document_intelligence_extractor` → `recursive_text_chunker` → `entity_extractor` → `azure_openai_agent` → `document_set_initializer` → `cross_document_comparison` → `azure_blob_output` - -```yaml -name: knowledge-graph -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: corporate-documents - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - settings: - doc_intelligence_endpoint: "${AZURE_DOC_INTELLIGENCE_ENDPOINT}" - model_id: prebuilt-layout - extract_text: true - - - executor: recursive_text_chunker - settings: - chunk_size: 2000 - chunk_overlap: 100 - input_field: doc_intell_output.text - - - executor: entity_extractor - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - entity_types: ["person", "organization", "product", "location", "project", "technology"] - output_field: entities - - - executor: azure_openai_agent - settings: - endpoint: "${AZURE_OPENAI_ENDPOINT}" - deployment_name: gpt-4.1 - instructions: | - From the extracted entities, identify relationships such as: - works_at, manages, reports_to, located_in, provides, uses, competes_with - Return as structured JSON with relationship triples. - output_field: relationships - - - executor: azure_blob_output - settings: - blob_container_name: knowledge-graphs - output_format: json -``` - ---- - -## 8. Included Sample Pipelines - -The repository includes 28+ ready-to-run sample pipelines in `contentflow-lib/samples/`: - -| Folder | Sample Name | What it demonstrates | -|---|---|---| -| `01-simple/` | Simple pipeline | Minimal pipeline: content retrieval + Document Intelligence | -| `02-batch-processing/` | Batch processing | Processing multiple documents concurrently | -| `03-pdf-extractor_chunker/` | PDF + Chunking | PyMuPDF extraction + recursive text chunker | -| `04-word-extractor/` | Word extractor | `.docx` content extraction with metadata | -| `05-powerpoint-extractor/` | PowerPoint extractor | `.pptx` slide-by-slide extraction | -| `06-ai-analysis/` | AI analysis | GPT-4 analysis with configurable prompts | -| `07-embeddings/` | Embeddings | Azure OpenAI embedding generation | -| `08-content-understanding/` | Content Understanding | Azure Content Understanding prebuilt models | -| `09-blob-input/` | Blob input | Full blob discovery + retrieval + Document Intelligence | -| `10-table-row-splitter/` | Table row splitter | Splitting tables into per-row content items | -| `11-excel-extractor/` | Excel extractor | Full Excel workbook extraction | -| `12-field-transformation/` | Field transformation | Field mapping, selection, and normalization | -| `13-blob-output-sample/` | Blob output | Writing processed results to Blob Storage | -| `14-gpt-rag-ingestion/` | GPT-RAG ingestion | Complete end-to-end RAG knowledge base pipeline | -| `15-document-analysis/` | Document analysis | Full Document Intelligence analysis workflow | -| `16-spreadsheet-pipeline/` | Spreadsheet pipeline | Excel ingestion → row splitting → field normalization | -| `17-knowledge-graph/` | Knowledge graph | Entity + relationship extraction for graph construction | -| `18-web-scraping/` | Web scraping | Playwright-based crawling and content extraction | -| `19-sub-pipelines/` | Sub-pipelines | Pipeline composition and nested workflow execution | -| `20-document-set-static/` | Document set (static) | Fixed multi-document cross-analysis | -| `21-document-set-comparison/` | Document comparison | Side-by-side AI comparison of two documents | -| `22-document-set-dynamic/` | Document set (dynamic) | Dynamically assembled document sets | -| `23-inline-document-set/` | Inline document set | Document sets defined inline in the pipeline | -| `27-subpipeline-processing/` | Subpipeline processing | Advanced sub-pipeline patterns | -| `28-advanced-batch/` | Advanced batch | Complex batch processing with error handling | -| `32-parallel-processing/` | Parallel processing | Fan-out/fan-in with concurrent branches | -| `44-conditional-routing/` | Conditional routing | If/else branching based on content properties | - -**Parallel workflow patterns** (documented in `PARALLEL_WORKFLOWS_GUIDE.md`): -- `fan_out_fan_in.yaml` — Split into parallel branches, merge results -- `split_merge.yaml` — Split content list, process in parallel, collect results -- `batch_subworkflow_example.yaml` — Batch processing with nested subworkflows -- `conditional_routing.yaml` — Content-aware routing based on classification results - ---- - -*Document version: March 25, 2026 — Based on ContentFlow repository branch `local-documentation`* diff --git a/Analysis/01-overview.md b/Analysis/01-overview.md deleted file mode 100644 index 9a13436..0000000 --- a/Analysis/01-overview.md +++ /dev/null @@ -1,430 +0,0 @@ -# ContentFlow — Guía General de la Solución - -> **Plataforma inteligente de procesamiento de documentos y contenido construida sobre Azure.** - ---- - -## Índice - -- [1. ¿Qué es ContentFlow?](#1-qué-es-contentflow) -- [2. ¿Qué problema resuelve?](#2-qué-problema-resuelve) -- [3. Conceptos clave](#3-conceptos-clave) - - [3.1 Pipelines](#31-pipelines) - - [3.2 Executors (Ejecutores)](#32-executors-ejecutores) - - [3.3 Vaults (Bóvedas de documentos)](#33-vaults-bóvedas-de-documentos) - - [3.4 Modelo de Contenido](#34-modelo-de-contenido) -- [4. Componentes principales](#4-componentes-principales) - - [4.1 Interfaz Web (contentflow-web)](#41-interfaz-web-contentflow-web) - - [4.2 API REST (contentflow-api)](#42-api-rest-contentflow-api) - - [4.3 Workers de procesamiento (contentflow-worker)](#43-workers-de-procesamiento-contentflow-worker) - - [4.4 Librería de procesamiento (contentflow-lib)](#44-librería-de-procesamiento-contentflow-lib) - - [4.5 Infraestructura (infra)](#45-infraestructura-infra) -- [5. ¿Cómo funciona el flujo completo?](#5-cómo-funciona-el-flujo-completo) - - [5.1 Paso a paso: De un documento a datos inteligentes](#51-paso-a-paso-de-un-documento-a-datos-inteligentes) - - [5.2 Diagrama de flujo general](#52-diagrama-de-flujo-general) -- [6. Servicios de Azure utilizados](#6-servicios-de-azure-utilizados) -- [7. Modos de despliegue](#7-modos-de-despliegue) - - [7.1 Modo Básico](#71-modo-básico) - - [7.2 Modo AILZ (AI Landing Zone)](#72-modo-ailz-ai-landing-zone) -- [8. Ejemplo práctico: Pipeline de documentos PDF](#8-ejemplo-práctico-pipeline-de-documentos-pdf) -- [9. Resumen ejecutivo](#9-resumen-ejecutivo) - ---- - -## 1. ¿Qué es ContentFlow? - -**ContentFlow** es una plataforma empresarial cloud-native diseñada para transformar contenido no estructurado (PDFs, documentos Word, hojas de Excel, presentaciones PowerPoint, páginas web, etc.) en **datos inteligentes y accionables** mediante flujos de trabajo automatizados impulsados por servicios de inteligencia artificial de Azure. - -Piensa en ContentFlow como una **fábrica de procesamiento de documentos**: introduces documentos crudos por un extremo y, tras pasar por una serie de estaciones de procesamiento configurables (extracción, análisis, enriquecimiento, etc.), obtienes datos estructurados, indexados y listos para consumir. - ---- - -## 2. ¿Qué problema resuelve? - -Las organizaciones enfrentan desafíos comunes con su contenido: - -| Desafío | Cómo lo resuelve ContentFlow | -|---------|------------------------------| -| Documentos en múltiples formatos (PDF, Word, Excel...) | **Ejecutores de extracción** especializados para cada formato | -| Procesamiento manual y lento | **Pipelines automatizados** que procesan contenido sin intervención | -| Necesidad de análisis inteligente | **Integración con Azure AI** (GPT-4, Document Intelligence, embeddings) | -| Escalabilidad limitada | **Arquitectura distribuida** con workers paralelos y colas de mensajes | -| Dificultad para configurar flujos | **Editor visual** drag-and-drop + definición YAML | -| Seguridad empresarial | **Identidad administrada** (Managed Identity) sin contraseñas | - ---- - -## 3. Conceptos clave - -### 3.1 Pipelines - -Un **pipeline** es la unidad fundamental de trabajo en ContentFlow. Define una secuencia de pasos (ejecutores) que el contenido debe recorrer para ser procesado. Se definen en formato YAML: - -```yaml -name: procesar-pdfs -steps: - - executor: azure_blob_input_discovery # Descubre documentos - settings: - blob_container_name: documentos - - - executor: pdf_extractor # Extrae texto del PDF - - - executor: recursive_text_chunker # Divide en fragmentos - settings: - chunk_size: 1000 - - - executor: azure_openai_embeddings # Genera vectores (embeddings) - settings: - model: text-embedding-3-small - - - executor: azure_blob_output # Guarda resultados - settings: - blob_container_name: procesados -``` - -### 3.2 Executors (Ejecutores) - -Los **ejecutores** son las unidades de procesamiento individuales dentro de un pipeline. Cada ejecutor realiza una tarea específica. ContentFlow incluye **más de 35 ejecutores** organizados en categorías: - -| Categoría | Descripción | Ejemplos | -|-----------|-------------|----------| -| **Entrada** | Descubren contenido desde orígenes de datos | Blob Storage, sistema de archivos | -| **Extracción** | Parsean documentos y extraen texto | PDF, Word, Excel, PowerPoint, CSV | -| **Procesamiento IA** | Análisis inteligente con Azure AI | Embeddings, Document Intelligence, GPT-4 | -| **Transformación** | Manipulan y enriquecen datos | Chunking, mapeo de campos, agregación | -| **Salida** | Almacenan resultados procesados | Blob Storage, Azure AI Search | -| **Enrutamiento** | Lógica condicional y paralelismo | Fan-out/Fan-in, sub-pipelines | -| **Análisis de texto** | Procesamiento de lenguaje natural | Resumen, entidades, sentimiento, PII, idioma | -| **Especializado** | Tareas de dominio específico | Web scraping, grafos de conocimiento | - -### 3.3 Vaults (Bóvedas de documentos) - -Un **vault** es un contenedor lógico que agrupa documentos y los asocia a un pipeline específico. Cuando el vault está habilitado, los workers automáticamente descubren nuevos documentos y los procesan según el pipeline asociado. - -``` -Vault "Facturas 2024" - ├── Pipeline asociado: "procesar-facturas" - ├── Tags: [facturas, contabilidad] - ├── Documentos descubiertos: 1,247 - └── Estado: Habilitado ✓ -``` - -### 3.4 Modelo de Contenido - -Todo el contenido que fluye por un pipeline utiliza un **modelo estandarizado** (`Content`) que acumula datos a medida que pasa por cada ejecutor: - -``` -Content - ├── id: identificador único - ├── source: origen del documento (ej. blob://container/archivo.pdf) - ├── content: texto extraído - ├── metadata: información adicional del documento - ├── chunks: fragmentos de texto - ├── embeddings: vectores numéricos para búsqueda semántica - └── analysis: resultados de análisis IA -``` - ---- - -## 4. Componentes principales - -ContentFlow se compone de **5 componentes principales** que trabajan de forma coordinada: - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ ContentFlow │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌───────────┐ │ -│ │ Web │──▶│ API │──▶│ Workers │──▶│ Librería │ │ -│ │ (React) │ │ (FastAPI)│ │ (Python) │ │ (Python) │ │ -│ └──────────┘ └────┬─────┘ └─────┬─────┘ └───────────┘ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌──────────────────────────────┐ │ -│ │ Infraestructura Azure │ │ -│ │ (Cosmos DB, Blob, Queue, │ │ -│ │ AI Services, Container │ │ -│ │ Apps, App Configuration) │ │ -│ └──────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 4.1 Interfaz Web (contentflow-web) - -La **interfaz web** es una aplicación React moderna que permite a los usuarios interactuar visualmente con ContentFlow: - -- **Constructor visual de pipelines**: Editor drag-and-drop basado en ReactFlow para diseñar pipelines gráficamente -- **Editor YAML**: Editor de código Monaco con resaltado de sintaxis para definir pipelines -- **Gestión de Vaults**: Crear, editar y monitorear bóvedas de documentos -- **Galería de plantillas**: Pipelines pre-construidos listos para usar -- **Panel de ejecuciones**: Ver el estado y resultados del procesamiento en tiempo real - -**Tecnologías**: React 18, TypeScript, Vite, Tailwind CSS, Shadcn/ui, ReactFlow, Monaco Editor - -### 4.2 API REST (contentflow-api) - -El **servicio API** es el punto central de coordinación. Recibe solicitudes de la interfaz web y orquesta las operaciones: - -- **Gestión de pipelines**: Crear, leer, actualizar, eliminar y ejecutar pipelines -- **Gestión de vaults**: Administrar bóvedas de documentos -- **Catálogo de ejecutores**: Explorar los ejecutores disponibles y sus configuraciones -- **Verificación de salud**: Monitorear el estado de todos los servicios dependientes - -**Tecnologías**: FastAPI, Python 3.12+, Uvicorn, Pydantic, Azure SDK - -**Puerto**: 8090 - -### 4.3 Workers de procesamiento (contentflow-worker) - -Los **workers** son el motor de ejecución distribuida. Procesan el contenido de forma asíncrona utilizando múltiples procesos: - -- **Workers de entrada (Source Workers)**: Descubren contenido nuevo en los orígenes de datos configurados -- **Workers de procesamiento (Processing Workers)**: Ejecutan los pipelines sobre cada elemento de contenido - -**Funcionamiento simplificado**: -``` -Worker de Entrada Worker de Procesamiento - │ │ - ├── Lee vaults habilitados ├── Consulta la cola - ├── Ejecuta ejecutores de entrada ├── Recibe tarea - ├── Descubre documentos nuevos ├── Lee el pipeline - └── Crea tareas en la cola └── Ejecuta pipeline - sobre el contenido -``` - -**Tecnologías**: Python 3.12+, multiprocessing, Azure Storage Queue - -**Puerto**: 8099 (API de salud) - -### 4.4 Librería de procesamiento (contentflow-lib) - -La **librería** es el corazón técnico de ContentFlow. Contiene: - -- **Motor de pipelines**: Parsea YAML y ejecuta grafos dirigidos acíclicos (DAG) -- **+35 ejecutores**: Implementaciones listas para usar -- **Modelo de contenido**: Estructura de datos estandarizada -- **Conectores Azure**: Clientes para Blob, Cosmos DB, AI Search, OpenAI, Document Intelligence - -Esta librería se instala como dependencia en el API y el Worker. - -### 4.5 Infraestructura (infra) - -Plantillas **Bicep** (Infrastructure as Code) para aprovisionar todos los recursos de Azure necesarios. Se despliega con un solo comando `azd up`. - ---- - -## 5. ¿Cómo funciona el flujo completo? - -### 5.1 Paso a paso: De un documento a datos inteligentes - -``` - ① USUARIO ② API ③ COLA - ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ - │ Crea o edita│ ─────────▶ │ Almacena en │ ─────────▶ │ Encola │ - │ un pipeline │ REST API │ Cosmos DB y │ Storage │ tarea de │ - │ y ejecuta │ │ crea tarea │ Queue │ descubrir │ - └─────────────┘ └─────────────┘ └──────┬──────┘ - │ - ⑥ RESULTADOS ⑤ PROCESAMIENTO ④ DESCUBRIMIENTO - ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ - │ Datos en │ ◀───────── │ Ejecuta cada│ ◀───────── │ Worker lee │ - │ Blob Storage│ Guardado │ ejecutor del│ Cola de │ el origen y │ - │ y Cosmos DB │ │ pipeline │ tareas │ lista docs │ - └─────────────┘ └─────────────┘ └─────────────┘ -``` - -**Flujo detallado**: - -1. **El usuario** crea un pipeline (visualmente o en YAML) y lo ejecuta desde la interfaz web -2. **La API** recibe la solicitud, guarda la configuración en Cosmos DB y crea un registro de ejecución -3. **La API** envía una tarea de tipo `InputSource` a la cola de Azure Storage -4. **El Worker de Entrada** lee la tarea, ejecuta los ejecutores de entrada del pipeline y descubre los documentos disponibles -5. **Por cada documento** descubierto, el Worker de Entrada crea una tarea `ContentProcessing` en la cola -6. **Los Workers de Procesamiento** toman las tareas de la cola y ejecutan el pipeline completo (extracción → transformación → análisis → salida) sobre cada documento -7. **Los resultados** se almacenan en Blob Storage y/o Cosmos DB -8. **El usuario** puede monitorear el progreso y ver los resultados desde la interfaz web - -### 5.2 Diagrama de flujo general - -``` -┌─────────────┐ HTTP/REST ┌─────────────────┐ -│ │ ──────────────────▶ │ │ -│ Frontend │ │ API Service │ -│ (React) │ ◀────────────────── │ (FastAPI) │ -│ │ Respuestas │ │ -└─────────────┘ └───────┬─────────┘ - │ - ┌───────────────┼──────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌────────────┐ ┌────────────┐ ┌──────────────┐ - │ Cosmos DB │ │ Blob │ │ Storage │ - │ (Metadatos │ │ Storage │ │ Queue │ - │ y estado) │ │(Documentos)│ │ (Tareas) │ - └──────┬─────┘ └─────┬──────┘ └───────┬──────┘ - │ │ │ - ▼ ▼ ▼ - ┌─────────────────────────────────────────────────┐ - │ Worker Service │ - │ ┌─────────────────┐ ┌──────────────────────┐ │ - │ │ Source Workers │ │ Processing Workers │ │ - │ │ (Descubrimiento) │ │ (Ejecución pipeline) │ │ - │ └────────┬────────┘ └──────────┬───────────┘ │ - │ │ │ │ - │ ▼ ▼ │ - │ ┌────────────────────────────────────────────┐ │ - │ │ contentflow-lib │ │ - │ │ (Motor de pipelines + 35+ Ejecutores) │ │ - │ └────────────────────┬───────────────────────┘ │ - └───────────────────────┼─────────────────────────┘ - │ - ▼ - ┌──────────────────────────┐ - │ Azure AI Services │ - │ ┌──────┐ ┌──────────┐ │ - │ │GPT-4 │ │ Document │ │ - │ │OpenAI│ │ Intelli- │ │ - │ │ │ │ gence │ │ - │ └──────┘ └──────────┘ │ - └──────────────────────────┘ -``` - ---- - -## 6. Servicios de Azure utilizados - -| Servicio de Azure | Propósito en ContentFlow | SKU / Nivel | -|-------------------|--------------------------|-------------| -| **Azure Container Apps** | Hospeda los 3 servicios (API, Worker, Web) | Consumption (serverless) | -| **Azure Cosmos DB** | Base de datos para metadatos, pipelines, ejecuciones | Serverless | -| **Azure Blob Storage** | Almacena documentos y contenido procesado | Standard_LRS, Hot | -| **Azure Storage Queue** | Cola de mensajes para distribución de tareas | Incluido en Storage | -| **Azure App Configuration** | Configuración centralizada de todos los servicios | Standard | -| **Azure Container Registry** | Registro de imágenes Docker | Standard (Premium con endpoints privados) | -| **Azure Log Analytics** | Workspace de logs y telemetría | PerGB2018 | -| **Azure Application Insights** | Monitoreo y diagnósticos | Conectado a Log Analytics | -| **Azure AI Foundry** | Plataforma de modelos de IA | S0 | -| **GPT-4.1 / GPT-4.1-mini** | Modelos de lenguaje para análisis inteligente | GlobalStandard, Capacidad: 100 | -| **Azure Document Intelligence** | Extracción inteligente de documentos (OCR) | Según integración | -| **Managed Identity** | Autenticación sin contraseñas entre servicios | User-Assigned | - ---- - -## 7. Modos de despliegue - -ContentFlow soporta dos modos de despliegue para adaptarse a diferentes necesidades: - -### 7.1 Modo Básico - -Ideal para **desarrollo, pruebas y demos**: - -- Endpoints públicos accesibles desde internet -- Red virtual creada automáticamente -- Todos los recursos se crean nuevos -- Sin endpoints privados -- Despliegue rápido y sencillo - -```bash -DEPLOYMENT_MODE=basic -azd up -``` - -### 7.2 Modo AILZ (AI Landing Zone) - -Diseñado para **entornos de producción empresarial**: - -- Se integra con una Azure AI Landing Zone existente -- Endpoints privados para todos los servicios -- Red virtual compartida con subnets dedicadas -- Zonas DNS privadas para resolución interna -- Cumplimiento con estándares de seguridad empresarial - -```bash -DEPLOYMENT_MODE=ailz-integrated -azd up -``` - -``` -┌─────────────────────────────────────────────────────────┐ -│ AI Landing Zone VNet │ -│ │ -│ ┌──────────────────┐ ┌──────────────────────────┐ │ -│ │ aca-env-subnet │ │ pe-subnet │ │ -│ │ ┌────────────┐ │ │ ┌──────┐ ┌──────────┐ │ │ -│ │ │ Container │ │ │ │Cosmos│ │ Storage │ │ │ -│ │ │ Apps (API, │ │ │ │ DB │ │ Account │ │ │ -│ │ │ Worker,Web)│ │ │ │(PE) │ │ (PE) │ │ │ -│ │ └────────────┘ │ │ └──────┘ └──────────┘ │ │ -│ └──────────────────┘ └──────────────────────────┘ │ -│ │ -│ PE = Private Endpoint │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## 8. Ejemplo práctico: Pipeline de documentos PDF - -A continuación, un ejemplo concreto de cómo ContentFlow procesa un conjunto de documentos PDF para crear un índice de búsqueda inteligente: - -**Pipeline: "Indexación de documentos PDF"** - -```yaml -name: indexacion-pdf -steps: - # 1. Descubrir PDFs en Blob Storage - - executor: azure_blob_input_discovery - settings: - blob_container_name: documentos-legales - file_extensions: [".pdf"] - - # 2. Recuperar contenido del blob - - executor: azure_blob_content_retriever - - # 3. Extraer texto con Azure Document Intelligence - - executor: azure_document_intelligence_extractor - settings: - model_id: prebuilt-layout - - # 4. Dividir en fragmentos manejables - - executor: recursive_text_chunker - settings: - chunk_size: 1000 - chunk_overlap: 200 - - # 5. Generar vectores para búsqueda semántica - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - # 6. Indexar en Azure AI Search - - executor: ai_search_index_output - settings: - index_name: documentos-legales-index -``` - -**Resultado**: Cada PDF se convierte en fragmentos indexados con vectores semánticos, listos para búsquedas inteligentes tipo "¿cuáles son las cláusulas de rescisión?". - ---- - -## 9. Resumen ejecutivo - -| Aspecto | Detalle | -|---------|---------| -| **Tipo de solución** | Plataforma de procesamiento de contenido empresarial | -| **Arquitectura** | Microservicios, event-driven, cloud-native | -| **Nube** | Microsoft Azure | -| **Servicios** | 3 contenedores: API, Worker, Web | -| **Motor de IA** | GPT-4.1, Document Intelligence, Embeddings | -| **Base de datos** | Cosmos DB (Serverless) | -| **Almacenamiento** | Azure Blob Storage | -| **Mensajería** | Azure Storage Queue | -| **Ejecutores disponibles** | 35+ ejecutores pre-construidos | -| **Formatos soportados** | PDF, Word, Excel, PowerPoint, CSV, HTML, web | -| **Seguridad** | Managed Identity, RBAC, sin contraseñas | -| **Despliegue** | `azd up` — un solo comando | -| **Modos** | Básico (desarrollo) o AILZ (producción empresarial) | - ---- - -> **ContentFlow** simplifica el camino desde documentos crudos hasta datos inteligentes, combinando la potencia de Azure AI con una arquitectura escalable y una experiencia de usuario intuitiva. diff --git a/Analysis/02-architecture-detailed.md b/Analysis/02-architecture-detailed.md deleted file mode 100644 index a3f9f6e..0000000 --- a/Analysis/02-architecture-detailed.md +++ /dev/null @@ -1,1920 +0,0 @@ -# ContentFlow — Arquitectura Detallada de Componentes - -> **Documentación técnica completa**: componentes, interconexiones, flujos de datos, SKUs, configuración y diagramas de la plataforma ContentFlow. - ---- - -## Índice - -- [1. Visión general de la arquitectura](#1-visión-general-de-la-arquitectura) - - [1.1 Diagrama de arquitectura completa](#11-diagrama-de-arquitectura-completa) - - [1.2 Principios arquitectónicos](#12-principios-arquitectónicos) -- [2. Componente: Interfaz Web (contentflow-web)](#2-componente-interfaz-web-contentflow-web) - - [2.1 Stack tecnológico](#21-stack-tecnológico) - - [2.2 Estructura de la aplicación](#22-estructura-de-la-aplicación) - - [2.3 Rutas y navegación](#23-rutas-y-navegación) - - [2.4 Componentes principales de la UI](#24-componentes-principales-de-la-ui) - - [2.5 Gestión de estado y datos](#25-gestión-de-estado-y-datos) - - [2.6 Diagrama de componentes Web](#26-diagrama-de-componentes-web) -- [3. Componente: API REST (contentflow-api)](#3-componente-api-rest-contentflow-api) - - [3.1 Stack tecnológico](#31-stack-tecnológico) - - [3.2 Endpoints del API](#32-endpoints-del-api) - - [3.3 Capa de servicios](#33-capa-de-servicios) - - [3.4 Capa de base de datos](#34-capa-de-base-de-datos) - - [3.5 Modelos de datos](#35-modelos-de-datos) - - [3.6 Inyección de dependencias](#36-inyección-de-dependencias) - - [3.7 Configuración y arranque](#37-configuración-y-arranque) - - [3.8 Diagrama de capas del API](#38-diagrama-de-capas-del-api) -- [4. Componente: Worker Service (contentflow-worker)](#4-componente-worker-service-contentflow-worker) - - [4.1 Stack tecnológico](#41-stack-tecnológico) - - [4.2 Arquitectura multi-proceso](#42-arquitectura-multi-proceso) - - [4.3 Worker de entrada (Input Source Worker)](#43-worker-de-entrada-input-source-worker) - - [4.4 Worker de procesamiento (Content Processing Worker)](#44-worker-de-procesamiento-content-processing-worker) - - [4.5 Cliente de cola](#45-cliente-de-cola) - - [4.6 Ciclo de vida de una tarea](#46-ciclo-de-vida-de-una-tarea) - - [4.7 Configuración del Worker](#47-configuración-del-worker) - - [4.8 Diagrama del motor de workers](#48-diagrama-del-motor-de-workers) -- [5. Componente: Librería Core (contentflow-lib)](#5-componente-librería-core-contentflow-lib) - - [5.1 Motor de pipelines](#51-motor-de-pipelines) - - [5.2 Jerarquía de clases de ejecutores](#52-jerarquía-de-clases-de-ejecutores) - - [5.3 Catálogo completo de ejecutores](#53-catálogo-completo-de-ejecutores) - - [5.4 Modelo de contenido (Content)](#54-modelo-de-contenido-content) - - [5.5 Conectores de Azure](#55-conectores-de-azure) - - [5.6 Diagrama del motor de pipelines](#56-diagrama-del-motor-de-pipelines) -- [6. Infraestructura de Azure (infra)](#6-infraestructura-de-azure-infra) - - [6.1 Recursos desplegados](#61-recursos-desplegados) - - [6.2 SKUs y niveles de servicio](#62-skus-y-niveles-de-servicio) - - [6.3 Módulos Bicep](#63-módulos-bicep) - - [6.4 Cosmos DB — Estructura de base de datos](#64-cosmos-db--estructura-de-base-de-datos) - - [6.5 Storage Account — Blobs y colas](#65-storage-account--blobs-y-colas) - - [6.6 Container Apps — Configuración de servicios](#66-container-apps--configuración-de-servicios) - - [6.7 AI Foundry — Modelos de IA](#67-ai-foundry--modelos-de-ia) - - [6.8 Diagrama de infraestructura Azure](#68-diagrama-de-infraestructura-azure) -- [7. Interconexión entre componentes](#7-interconexión-entre-componentes) - - [7.1 Flujo de datos completo](#71-flujo-de-datos-completo) - - [7.2 Flujo de mensajes por cola](#72-flujo-de-mensajes-por-cola) - - [7.3 Flujo de autenticación](#73-flujo-de-autenticación) - - [7.4 Matriz de comunicación entre servicios](#74-matriz-de-comunicación-entre-servicios) - - [7.5 Diagrama de secuencia: Ejecución de pipeline](#75-diagrama-de-secuencia-ejecución-de-pipeline) -- [8. Modos de despliegue](#8-modos-de-despliegue) - - [8.1 Modo básico](#81-modo-básico) - - [8.2 Modo AILZ (AI Landing Zone)](#82-modo-ailz-ai-landing-zone) - - [8.3 Proceso de despliegue con azd](#83-proceso-de-despliegue-con-azd) - - [8.4 Diagrama comparativo de modos](#84-diagrama-comparativo-de-modos) -- [9. Seguridad y autenticación](#9-seguridad-y-autenticación) - - [9.1 Modelo de identidad](#91-modelo-de-identidad) - - [9.2 Roles RBAC asignados](#92-roles-rbac-asignados) - - [9.3 Diagrama de seguridad](#93-diagrama-de-seguridad) -- [10. Configuración centralizada](#10-configuración-centralizada) - - [10.1 Azure App Configuration](#101-azure-app-configuration) - - [10.2 Variables de entorno por servicio](#102-variables-de-entorno-por-servicio) -- [11. Monitoreo y observabilidad](#11-monitoreo-y-observabilidad) -- [12. Resumen de dependencias entre componentes](#12-resumen-de-dependencias-entre-componentes) - ---- - -## 1. Visión general de la arquitectura - -ContentFlow implementa una **arquitectura de microservicios event-driven (basada en eventos)** con los siguientes patrones clave: - -- **Microservicios**: 3 servicios independientes (API, Worker, Web) desplegados como contenedores -- **Event-Driven**: Comunicación asíncrona mediante colas de mensajes -- **Cloud-Native**: Diseñado para Azure Container Apps con escalado automático -- **Declarativo**: Pipelines definidos en YAML, infraestructura definida en Bicep - -### 1.1 Diagrama de arquitectura completa - -``` -┌──────────────────────────────────────────────────────────────────────────────────┐ -│ AZURE CONTAINER APPS ENVIRONMENT │ -│ │ -│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ -│ │ contentflow-web │ │ contentflow-api │ │contentflow-worker│ │ -│ │ │ │ │ │ │ │ -│ │ React 18 │ │ FastAPI │ │ Multi-process │ │ -│ │ TypeScript │───▶│ Python 3.12+ │ │ Python 3.12+ │ │ -│ │ Vite │ │ Puerto: 8090 │ │ Puerto: 8099 │ │ -│ │ ReactFlow │ │ │ │ │ │ -│ │ Monaco Editor │ │ ┌────────────┐ │ │ ┌──────────────┐ │ │ -│ │ │ │ │ Routers │ │ │ │Source Workers│ │ │ -│ │ │ │ │ Services │ │ │ │Process Worker│ │ │ -│ │ │ │ │ Models │ │ │ │Queue Client │ │ │ -│ │ │ │ └────────────┘ │ │ └──────────────┘ │ │ -│ │ CPU: 1 | 2Gi │ │ CPU: 1 | 2Gi │ │ CPU: 1 | 2Gi │ │ -│ │ Réplicas: 1-2 │ │ Réplicas: 1-2 │ │ Réplicas: 1-2 │ │ -│ └──────────────────┘ └───────┬──────────┘ └────────┬─────────┘ │ -│ │ │ │ -└──────────────────────────────────┼─────────────────────────┼─────────────────────┘ - │ │ - ┌───────────────────────┼─────────────────────────┼──────────────┐ - │ ▼ ▼ │ - │ ┌─────────────┐ ┌─────────────┐ ┌──────────────────────┐ │ - │ │ Cosmos DB │ │ Blob │ │ Storage Queue │ │ - │ │ (Serverless)│ │ Storage │ │ (contentflow- │ │ - │ │ │ │ (Standard │ │ execution-requests) │ │ - │ │ 7 containers│ │ LRS, Hot) │ │ │ │ - │ └─────────────┘ └─────────────┘ └──────────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────┐ ┌────────────────────┐ │ - │ │ App Config │ │ App Insights │ │ AI Foundry (S0) │ │ - │ │ (Standard) │ │ + Log Analyt.│ │ GPT-4.1 │ │ - │ │ │ │ (PerGB2018) │ │ GPT-4.1-mini │ │ - │ └──────────────┘ └──────────────┘ └────────────────────┘ │ - │ │ - │ ┌──────────────┐ ┌──────────────┐ │ - │ │ Container │ │ Managed │ │ - │ │ Registry │ │ Identity │ │ - │ │ (Standard) │ │ (User Assign)│ │ - │ └──────────────┘ └──────────────┘ │ - │ │ - │ SERVICIOS DE AZURE │ - └────────────────────────────────────────────────────────────────┘ -``` - -### 1.2 Principios arquitectónicos - -| Principio | Implementación | -|-----------|---------------| -| **Desacoplamiento** | Los servicios se comunican a través de colas y base de datos compartida, no llamadas directas | -| **Escalabilidad horizontal** | Workers pueden escalarse independientemente (1-2 réplicas con autoescalado) | -| **Procesamiento asíncrono** | Las tareas se encolan y procesan en segundo plano | -| **Resiliencia** | Reintentos configurables (3 por defecto), timeouts, y manejo de errores por ejecutor | -| **Seguridad Zero-Trust** | Managed Identity, sin contraseñas compartidas, RBAC para cada recurso | -| **Configuración centralizada** | Azure App Configuration como fuente única de verdad | -| **Observabilidad** | Application Insights + Log Analytics para trazabilidad distribuida | - ---- - -## 2. Componente: Interfaz Web (contentflow-web) - -### 2.1 Stack tecnológico - -| Tecnología | Versión | Rol | -|------------|---------|-----| -| React | 18.3.1 | Framework UI | -| TypeScript | 5.8.3 | Lenguaje tipado | -| Vite | 7.1.5 | Empaquetador y servidor de desarrollo | -| Tailwind CSS | 3.4.17 | Framework de estilos utilitarios | -| ReactFlow | 11.11.4 | Visualización de grafos (pipeline builder) | -| Monaco Editor | 4.7.0 | Editor de código YAML | -| TanStack Query | 5.83.0 | Gestión de estado del servidor | -| React Hook Form | 7.61.1 | Formularios con validación | -| Zod | 3.25.76 | Esquemas de validación TypeScript | -| Shadcn/ui + Radix UI | Múltiples | Componentes UI accesibles | -| Recharts | 2.15.4 | Gráficas y visualizaciones | -| Lucide React | 0.462.0 | Iconografía | -| js-yaml | 4.1.1 | Parsing YAML | -| date-fns | 3.6.0 | Utilidades de fecha | - -### 2.2 Estructura de la aplicación - -``` -contentflow-web/src/ -├── main.tsx # Punto de entrada de React -├── App.tsx # Componente raíz con providers y rutas -├── index.css # Estilos globales (Tailwind) -├── components/ -│ ├── ui/ # Componentes base Shadcn/ui -│ │ ├── button.tsx # Botones -│ │ ├── dialog.tsx # Diálogos modales -│ │ ├── input.tsx # Campos de entrada -│ │ ├── select.tsx # Selectores -│ │ ├── toast.tsx # Notificaciones -│ │ ├── tabs.tsx # Pestañas -│ │ ├── card.tsx # Tarjetas -│ │ └── ... # +20 componentes más -│ ├── pipeline/ # Componentes del constructor de pipelines -│ ├── vaults/ # Componentes de gestión de vaults -│ ├── dashboard/ # Componentes del panel principal -│ ├── knowledge/ # Componentes del grafo de conocimiento -│ ├── templates/ # Componentes de plantillas -│ ├── Hero.tsx # Sección de bienvenida -│ ├── PipelineBuilder.tsx # Constructor visual de pipelines -│ ├── KnowledgeGraph.tsx # Visualizador de grafos -│ ├── Footer.tsx # Pie de página -│ └── NavLink.tsx # Enlace de navegación -├── pages/ -│ ├── Index.tsx # Página principal (home, pipeline, vaults) -│ ├── Templates.tsx # Galería de plantillas -│ ├── Vaults.tsx # Gestión de vaults (ruta directa) -│ └── NotFound.tsx # Página 404 -├── hooks/ # Custom hooks de React -├── types/ # Definiciones de tipos TypeScript -├── lib/ # Utilidades y configuración (cn, utils) -└── data/ # Datos mock y constantes -``` - -### 2.3 Rutas y navegación - -| Ruta | Componente | Vista | Descripción | -|------|-----------|-------|-------------| -| `/` | Index | home | Página principal con hero y navegación | -| `/?view=pipeline` | Index | pipeline | Constructor visual de pipelines | -| `/?view=graph` | Index | graph | Visualizador de grafos de conocimiento | -| `/?view=vaults` | Index | vaults | Gestión de bóvedas de documentos | -| `/templates` | Templates | — | Galería de plantillas pre-construidas | -| `/*` | NotFound | — | Página 404 | - -### 2.4 Componentes principales de la UI - -``` -┌────────────────────────────────────────────────────────────────┐ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Barra de Navegación │ │ -│ │ [Logo] ContentFlow | Home | Pipeline | Vaults | │ │ -│ │ Templates │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ CONTENIDO PRINCIPAL (según vista activa) │ │ -│ │ │ │ -│ │ ┌─ Home ──────────────────────────────────────────┐ │ │ -│ │ │ Hero: Bienvenida y métricas del sistema │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌─ Pipeline Builder ──────────────────────────────┐ │ │ -│ │ │ ┌───────────┐ ┌───────────────────────────┐ │ │ │ -│ │ │ │ Catálogo │ │ Lienzo ReactFlow │ │ │ │ -│ │ │ │ de │ │ (Nodos + Aristas) │ │ │ │ -│ │ │ │ ejecutores│ │ │ │ │ │ -│ │ │ │ │ │ [Nodo A] ──▶ [Nodo B] │ │ │ │ -│ │ │ │ • Input │ │ │ │ │ │ │ -│ │ │ │ • Extract │ │ ▼ │ │ │ │ -│ │ │ │ • Process │ │ [Nodo C] ──▶ [Nodo D] │ │ │ │ -│ │ │ │ • Output │ │ │ │ │ │ -│ │ │ └───────────┘ └───────────────────────────┘ │ │ │ -│ │ │ ┌─────────────────────────────────────────────┐│ │ │ -│ │ │ │ Editor YAML (Monaco) ││ │ │ -│ │ │ └─────────────────────────────────────────────┘│ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌─ Vaults ────────────────────────────────────────┐ │ │ -│ │ │ Lista de bóvedas │ Detalle │ Ejecuciones │ │ │ -│ │ └─────────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ Footer │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└────────────────────────────────────────────────────────────────┘ -``` - -### 2.5 Gestión de estado y datos - -| Librería | Responsabilidad | Patrón | -|----------|----------------|--------| -| **TanStack Query** | Estado del servidor (datos de la API) | Cache con invalidación automática | -| **React Hook Form** | Estado de formularios | Formularios controlados con validación | -| **Zod** | Validación de esquemas | Validación en tiempo de ejecución | -| **URL Query Params** | Navegación y vista activa | Estado en la URL (ej. `?view=pipeline`) | -| **React Context** | Estado global de la app (si aplica) | Provider pattern | - -### 2.6 Diagrama de componentes Web - -``` - App.tsx - │ - ┌───────────┼───────────────┐ - │ │ │ - QueryClient BrowserRouter Providers - Provider │ (Tooltip, - │ │ Toast, - │ ┌─────┴──────┐ Sonner) - │ │ │ - │ Route / Route /templates - │ │ │ - │ Index Templates - │ │ - │ ├── Hero - │ ├── PipelineBuilder ──▶ ReactFlow + Monaco - │ ├── KnowledgeGraph ──▶ Visualización de grafos - │ └── Vaults ──▶ CRUD de bóvedas - │ - └── Comunicación con API vía fetch/httpx - (GET/POST/PUT/DELETE → /api/*) -``` - ---- - -## 3. Componente: API REST (contentflow-api) - -### 3.1 Stack tecnológico - -| Tecnología | Versión | Rol | -|------------|---------|-----| -| Python | 3.12+ | Runtime | -| FastAPI | 0.128.0 | Framework web asíncrono | -| Uvicorn | 0.40.0 | Servidor ASGI | -| Pydantic | 2.12+ | Validación de datos y modelos | -| pydantic-settings | 2.12.0 | Gestión de configuración | -| azure-identity | 1.25.1 | Autenticación con Managed Identity | -| azure-cosmos | 4.14.3 | Cliente Cosmos DB NoSQL | -| azure-storage-blob | 12.27.1 | Cliente Blob Storage | -| azure-storage-queue | 12.14.1 | Cliente Storage Queue | -| azure-appconfiguration-provider | 2.3.1 | Configuración centralizada | -| httpx | 0.28.1 | Cliente HTTP asíncrono | -| aiohttp | 3.13.3+ | Cliente HTTP asíncrono alternativo | -| PyYAML | 6.0.3+ | Parsing YAML | -| python-dotenv | 1.2.1+ | Variables de entorno locales | -| contentflow-lib | local | Librería de procesamiento (editable) | - -### 3.2 Endpoints del API - -#### Salud del sistema - -| Método | Ruta | Descripción | Respuesta | -|--------|------|-------------|-----------| -| `GET` | `/api/health/` | Verificación completa de todos los servicios | `SystemHealth` | -| `GET` | `/api/health/{service}` | Verificar un servicio específico | `ServiceHealth` | - -Servicios verificados: `cosmos_db`, `storage_queue`, `app_config` - -#### Gestión de Pipelines - -| Método | Ruta | Descripción | Request Body | Respuesta | -|--------|------|-------------|-------------|-----------| -| `GET` | `/api/pipelines/` | Listar todos los pipelines | — | `List[Pipeline]` | -| `GET` | `/api/pipelines/{id_or_name}` | Obtener por ID o nombre | — | `Pipeline` | -| `POST` | `/api/pipelines/` | Crear o actualizar pipeline | `SavePipelineRequest` | `Pipeline` | -| `DELETE` | `/api/pipelines/{id}` | Eliminar pipeline | — | `bool` | -| `POST` | `/api/pipelines/{id}/execute` | Ejecutar pipeline | `ExecutePipelineRequest` | `ExecutePipelineResponse` | - -#### Gestión de Vaults - -| Método | Ruta | Descripción | Parámetros | Respuesta | -|--------|------|-------------|-----------|-----------| -| `GET` | `/api/vaults/` | Listar vaults | `search`, `tags` | `List[Vault]` | -| `GET` | `/api/vaults/{id}` | Obtener vault por ID | — | `Vault` | -| `POST` | `/api/vaults/` | Crear vault | Body: `VaultCreateRequest` | `Vault` | -| `PUT` | `/api/vaults/{id}` | Actualizar vault | Body: `VaultUpdateRequest` | `Vault` | -| `DELETE` | `/api/vaults/{id}` | Eliminar vault | — | `bool` | - -#### Catálogo de Ejecutores - -| Método | Ruta | Descripción | Respuesta | -|--------|------|-------------|-----------| -| `GET` | `/api/executors/` | Listar todos los ejecutores | `List[ExecutorCatalogDefinition]` | -| `GET` | `/api/executors/{id}` | Obtener ejecutor por ID | `ExecutorCatalogDefinition` | - -#### Raíz - -| Método | Ruta | Descripción | -|--------|------|-------------| -| `GET` | `/` | Mensaje de bienvenida y versión del API | - -### 3.3 Capa de servicios - -Cada servicio implementa lógica de negocio específica heredando de `BaseService`: - -``` -BaseService (CRUD genérico sobre Cosmos DB) - │ - ├── PipelineService - │ ├── get_pipelines() → List[Pipeline] - │ ├── get_pipeline_by_id(id) → Pipeline - │ ├── get_pipeline_by_name(name) → Pipeline - │ ├── create_pipeline(data) → Pipeline - │ ├── create_or_save_pipeline(data) → Pipeline - │ ├── update_by_id(id, data) → Pipeline - │ └── delete_pipeline_by_id(id) → bool - │ - ├── VaultService - │ ├── create_vault(request, pipeline_name) → Vault - │ ├── get_vault(id) → Vault - │ ├── list_vaults(search, tags) → List[Vault] - │ ├── update_vault(id, request) → Vault - │ └── delete_vault(id) → bool - │ - ├── VaultExecutionService - │ ├── get_vault_executions(vault_id, start, end) → List[VaultExecution] - │ ├── get_vault_crawl_checkpoints(vault_id) → List[Checkpoint] - │ ├── delete_vault_executions(vault_id) - │ └── delete_vault_crawl_checkpoints(vault_id) - │ - ├── PipelineExecutionService - │ └── Seguimiento de ejecuciones de pipeline - │ - ├── ExecutorCatalogService - │ └── Gestión del catálogo de ejecutores - │ - └── HealthService - ├── check_all_services() → SystemHealth - └── check_service_health(name) → ServiceHealth -``` - -### 3.4 Capa de base de datos - -**CosmosDBClient** — Wrapper asíncrono sobre el SDK oficial de Azure Cosmos DB: - -``` -CosmosDBClient - │ - ├── connect() # Inicializar conexión con credenciales - ├── close() # Cerrar conexión - │ - ├── create(container, item) # Crear documento - ├── get_by_id(container, id) # Obtener por ID - ├── list_all(container, query) # Listar con consulta SQL - ├── query(container, query, params) # Consulta parametrizada - ├── update(container, item) # Upsert (crear o actualizar) - ├── delete(container, id) # Eliminar - └── batch_upsert(container, items) # Actualización masiva -``` - -**Características**: -- Autenticación via `ChainedTokenCredential` (Managed Identity) -- Creación automática de la base de datos si no existe -- Consultas cross-partition habilitadas -- Pool de conexiones compartido - -### 3.5 Modelos de datos - -``` -CosmosBaseModel (Base) - │ - ├── id: str (UUID v4 auto-generado) - ├── created_at: datetime - └── modified_at: datetime - -Pipeline (Definición de pipeline) - ├── name: str - ├── description: str - ├── yaml: str (definición YAML del pipeline) - ├── nodes: List[Any] (nodos del grafo visual) - ├── edges: List[Any] (aristas del grafo visual) - ├── tags: List[str] - ├── enabled: bool - ├── retry_delay: int (segundos, default: 5) - ├── timeout: int (segundos, default: 600) - ├── retries: int (default: 3) - └── version: str - -PipelineExecution (Ejecución de pipeline) - ├── pipeline_id: str - ├── pipeline_name: str - ├── status: ExecutionStatus (pending|running|completed|failed|cancelled) - ├── inputs: Dict - ├── configuration: Dict - ├── outputs: Dict - ├── started_at: str (ISO) - ├── completed_at: str (ISO) - ├── duration_seconds: float - ├── error: str - ├── executor_outputs: Dict[str, ExecutorOutput] - └── events: List[PipelineExecutionEvent] - -Vault (Bóveda de documentos) - ├── name: str (1-100 caracteres) - ├── description: str (max 500) - ├── pipeline_id: str - ├── pipeline_name: str - ├── tags: List[str] - ├── save_execution_output: bool - ├── enabled: bool - └── created_by: str - -VaultExecution (Ejecución de vault) - ├── pipeline_id / vault_id: str - ├── status: pending|running|completed|failed - ├── task_id: str - ├── source_worker_id / processing_worker_id: str - ├── executor_outputs: Dict - ├── events: List[Dict] - ├── content: List[Dict] - └── number_of_items: int - -VaultCrawlCheckpoint (Punto de control de rastreo) - ├── pipeline_id / vault_id / executor_id: str - ├── checkpoint_timestamp: str - └── worker_id: str -``` - -### 3.6 Inyección de dependencias - -FastAPI utiliza inyección de dependencias para proporcionar clientes compartidos a los routers: - -``` -get_config_provider() ──▶ ConfigurationProvider (Azure App Config) - │ │ - ▼ ▼ -get_cosmos_client() ──▶ CosmosDBClient (singleton) - │ - ├──▶ get_pipeline_service() ──▶ PipelineService - ├──▶ get_vault_service() ──▶ VaultService - ├──▶ get_vault_execution_service() ──▶ VaultExecutionService - ├──▶ get_executor_catalog_service() ──▶ ExecutorCatalogService - ├──▶ get_pipeline_execution_service() ──▶ PipelineExecutionService - └──▶ get_health_service() ──▶ HealthService - -Todas las dependencias usan un TTL de caché de 10 minutos. -``` - -### 3.7 Configuración y arranque - -**Fuentes de configuración** (en orden de prioridad): - -1. **Variables de entorno** (ej. `.env` para desarrollo local) -2. **Azure App Configuration** (producción) -3. **Valores por defecto** (hardcoded en `settings.py`) - -**Secuencia de arranque del API**: - -``` -main.py - │ - ├── 1. Crear aplicación FastAPI - ├── 2. Configurar CORS (allow_origins: ["*"]) - ├── 3. Registrar routers (/api/health, /api/pipelines, /api/vaults, /api/executors) - │ - └── Lifespan (async context manager): - ├── STARTUP: - │ ├── initialize_cosmos() → Conectar a Cosmos DB - │ ├── initialize_blob_storage() → Inicializar Blob Storage - │ └── initialize_executor_catalog() → Cargar catálogo de ejecutores - │ - └── SHUTDOWN: - └── Limpieza de recursos -``` - -**Configuración del servidor**: - -| Parámetro | Valor | Descripción | -|-----------|-------|-------------| -| Host | `0.0.0.0` | Escuchar en todas las interfaces | -| Puerto | `8090` | Puerto HTTP | -| Workers | `1` | Un proceso Uvicorn | -| Reload | `True` (en DEBUG) | Recarga automática en desarrollo | -| OpenAPI docs | `/docs` | Documentación interactiva Swagger | -| ReDoc | `/redoc` | Documentación alternativa | - -### 3.8 Diagrama de capas del API - -``` -┌─────────────────────────────────────────────────────────┐ -│ CAPA DE PRESENTACIÓN │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ -│ │ /health │ │/pipelines│ │ /vaults │ │/executors │ │ -│ │ Router │ │ Router │ │ Router │ │ Router │ │ -│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ -│ │ │ │ │ │ -├───────┼────────────┼────────────┼──────────────┼─────────┤ -│ ▼ ▼ ▼ ▼ │ -│ CAPA DE SERVICIOS │ -│ │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────┐ │ -│ │ Health │ │ Pipeline │ │ Vault │ │ Catalog │ │ -│ │ Service │ │ Service │ │ Service │ │ Service │ │ -│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬─────┘ │ -│ │ │ │ │ │ -├───────┼────────────┼────────────┼──────────────┼─────────┤ -│ ▼ ▼ ▼ ▼ │ -│ CAPA DE DATOS │ -│ │ -│ ┌──────────────────────────────────────────────────┐ │ -│ │ CosmosDBClient (Async) │ │ -│ │ ┌─────────────────────────────────────────────┐ │ │ -│ │ │ ChainedTokenCredential (Managed Identity) │ │ │ -│ │ └─────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │ -│ │ BlobStorage │ │ StorageQueue │ │ AppConfig │ │ -│ │ Client │ │ Client │ │ Provider │ │ -│ └──────────────┘ └──────────────┘ └───────────────┘ │ -└─────────────────────────────────────────────────────────┘ -``` - ---- - -## 4. Componente: Worker Service (contentflow-worker) - -### 4.1 Stack tecnológico - -| Tecnología | Versión | Rol | -|------------|---------|-----| -| Python | 3.12+ | Runtime | -| multiprocessing | stdlib | Paralelismo de procesos | -| FastAPI | 0.128.0 | API de salud (opcional) | -| azure-storage-queue | 12.14.1 | Consumir tareas de la cola | -| azure-cosmos | 4.14.3 | Leer pipelines y actualizar estados | -| azure-storage-blob | 12.27.1 | Acceso a documentos | -| contentflow-lib | local | Motor de pipelines | - -### 4.2 Arquitectura multi-proceso - -El Worker utiliza el módulo `multiprocessing` de Python para ejecutar múltiples procesos independientes: - -``` -┌──────────────────────────────────────────────────────────┐ -│ WORKER ENGINE │ -│ │ -│ ┌────────────────────────────┐ │ -│ │ Proceso Principal │ │ -│ │ (main.py) │ │ -│ │ │ │ -│ │ ├── Signal Handler │ │ -│ │ ├── WorkerEngine.run() │ │ -│ │ └── API Thread (8099) │ │ -│ └─────────────┬──────────────┘ │ -│ │ │ -│ ┌───────────┴───────────────────────────┐ │ -│ │ │ │ -│ ▼ ▼ │ -│ ┌────────────────────────┐ ┌────────────────────────┐ │ -│ │ Source Worker(s) │ │ Processing Worker(s) │ │ -│ │ (InputSourceWorker) │ │ (ContentProcessing │ │ -│ │ │ │ Worker) │ │ -│ │ Cantidad: N_SOURCE │ │ Cantidad: N_PROCESS │ │ -│ │ Default: 1 │ │ Default: 0 │ │ -│ │ │ │ │ │ -│ │ Ciclo: │ │ Ciclo: │ │ -│ │ 1. Leer vaults │ │ 1. Poll cola │ │ -│ │ 2. Ejecutar inputs │ │ 2. Recibir tarea │ │ -│ │ 3. Descubrir contenido│ │ 3. Leer pipeline │ │ -│ │ 4. Crear tareas │ │ 4. Ejecutar pipeline │ │ -│ │ 5. Dormir (300s) │ │ 5. Actualizar estado │ │ -│ └────────────────────────┘ └────────────────────────┘ │ -│ │ -│ ┌───────────────────────────────────────────────────┐ │ -│ │ multiprocessing.Event (stop_event) │ │ -│ │ → Señal compartida para apagado coordinado │ │ -│ └───────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────┘ -``` - -### 4.3 Worker de entrada (Input Source Worker) - -**Responsabilidad**: Descubrir contenido nuevo en los orígenes de datos configurados. - -**Flujo de ejecución**: - -``` -┌─ Bucle principal ────────────────────────────────┐ -│ │ -│ 1. Consultar Cosmos DB → vaults habilitados │ -│ 2. Para cada vault: │ -│ a. Leer pipeline asociado │ -│ b. Obtener checkpoint más reciente │ -│ c. Ejecutar ejecutor(es) de input │ -│ d. Descubrir documentos nuevos │ -│ e. Por cada documento nuevo: │ -│ - Crear ContentProcessingTask │ -│ - Encolar en Storage Queue │ -│ f. Guardar nuevo checkpoint │ -│ 3. Dormir (DEFAULT_POLLING_INTERVAL_SECONDS) │ -│ 4. Repetir hasta stop_event │ -│ │ -└───────────────────────────────────────────────────┘ -``` - -**Gestión de checkpoints**: El sistema implementa **rastreo incremental** para evitar reprocesar documentos: -- En la primera ejecución, no hay checkpoint (se descubren todos los documentos) -- En ejecuciones posteriores, solo se descubren documentos nuevos o modificados desde el último checkpoint -- Los checkpoints se almacenan en el contenedor `vault_crawl_checkpoints` de Cosmos DB - -### 4.4 Worker de procesamiento (Content Processing Worker) - -**Responsabilidad**: Ejecutar pipelines completos sobre elementos de contenido individuales. - -**Flujo de ejecución**: - -``` -┌─ Bucle principal ────────────────────────────────┐ -│ │ -│ 1. Poll Azure Storage Queue │ -│ (max 32 mensajes, visibilidad 300s) │ -│ 2. Para cada mensaje: │ -│ a. Deserializar ContentProcessingTask │ -│ b. Obtener bloqueo distribuido │ -│ c. Leer pipeline de Cosmos DB │ -│ d. Crear PipelineExecutor │ -│ e. Ejecutar pipeline (sin ejecutores input) │ -│ f. Actualizar estado en Cosmos DB │ -│ g. Eliminar mensaje de la cola │ -│ 3. Si no hay mensajes → dormir (5s) │ -│ 4. Repetir hasta stop_event │ -│ │ -└───────────────────────────────────────────────────┘ -``` - -### 4.5 Cliente de cola - -**TaskQueueClient** — Abstracción sobre Azure Storage Queue: - -``` -TaskQueueClient - │ - ├── ensure_queue_exists() - │ → Crea la cola si no existe - │ - ├── send_content_processing_task(task: ContentProcessingTask) - │ → Serializa tarea → JSON → Base64 → Envía a cola - │ - ├── send_input_source_task(task: InputSourceTask) - │ → Serializa tarea → JSON → Base64 → Envía a cola - │ - └── _send_message(message, visibility_timeout) - → Envío interno con manejo de errores -``` - -**Formato del mensaje en cola**: -```json -{ - "task_type": "CONTENT_PROCESSING", - "payload": { - "pipeline_id": "abc-123", - "vault_id": "def-456", - "content": { ... } - } -} -``` - -### 4.6 Ciclo de vida de una tarea - -``` - ┌──────────┐ - │ PENDING │ ← Tarea creada y encolada - └────┬─────┘ - │ - ┌────▼─────┐ - │ RUNNING │ ← Worker toma la tarea - └────┬─────┘ - │ - ┌────────┼────────┐ - │ │ │ - ┌────▼───┐ ┌──▼───┐ ┌─▼──────┐ - │COMPLETED│ │FAILED│ │CANCELLED│ - └────────┘ └──────┘ └────────┘ - │ │ - │ ┌────▼────┐ - │ │ RETRY? │ (max 3 reintentos) - │ └────┬────┘ - │ │ Sí - │ ┌────▼─────┐ - │ │ PENDING │ - │ └──────────┘ - │ - Resultado almacenado - en Cosmos DB + Blob -``` - -### 4.7 Configuración del Worker - -| Parámetro | Default | Descripción | -|-----------|---------|-------------| -| `WORKER_NAME` | `contentflow-worker` | Nombre del worker | -| `NUM_PROCESSING_WORKERS` | `0` | Cantidad de workers de procesamiento | -| `NUM_SOURCE_WORKERS` | `1` | Cantidad de workers de entrada | -| `QUEUE_POLL_INTERVAL_SECONDS` | `5` | Intervalo de consulta a la cola | -| `QUEUE_VISIBILITY_TIMEOUT_SECONDS` | `300` | Timeout de visibilidad (5 min) | -| `QUEUE_MAX_MESSAGES` | `32` | Máximo de mensajes por poll | -| `DEFAULT_POLLING_INTERVAL_SECONDS` | `300` | Intervalo de descubrimiento (5 min) | -| `LOCK_TTL_SECONDS` | `300` | Tiempo de vida del bloqueo distribuido | -| `MAX_TASK_RETRIES` | `3` | Reintentos máximos por tarea | -| `TASK_TIMEOUT_SECONDS` | `600` | Timeout por tarea (10 min) | -| `API_ENABLED` | `true` | Habilitar API de salud | -| `API_PORT` | `8099` | Puerto del API de salud | - -### 4.8 Diagrama del motor de workers - -``` -┌─────────────────────────────────────────────────────────────┐ -│ WORKER ENGINE │ -│ │ -│ Señales del SO ──▶ Signal Handler ──▶ stop_event.set() │ -│ (SIGINT/SIGTERM) │ -│ │ -│ ┌────────────────────────────────────────────────────┐ │ -│ │ WorkerEngine.run() │ │ -│ │ ├── start() │ │ -│ │ │ ├── Crear N source workers (mp.Process) │ │ -│ │ │ └── Crear N processing workers (mp.Process) │ │ -│ │ │ │ │ -│ │ └── monitor loop (mientras no stop_event): │ │ -│ │ ├── Verificar procesos activos │ │ -│ │ ├── Reemplazar workers caídos │ │ -│ │ └── sleep(1 segundo) │ │ -│ └────────────────────────────────────────────────────┘ │ -│ │ -│ Apagado: │ -│ 1. stop_event.set() │ -│ 2. join(timeout=30s) por cada worker │ -│ 3. terminate() workers que no respondieron │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 5. Componente: Librería Core (contentflow-lib) - -### 5.1 Motor de pipelines - -El motor de pipelines es el cerebro de ContentFlow. Convierte definiciones YAML declarativas en grafos de ejecución: - -``` -YAML Pipeline Definition - │ - ▼ -┌─────────────────┐ -│ PipelineFactory │ ← Parsea YAML y crea instancias de ejecutores -│ │ -│ parse_yaml() │ -│ create_pipeline()│ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ DAG (Grafo │ ← Grafo dirigido acíclico de ejecutores -│ Dirigido │ -│ Acíclico) │ -│ │ -│ Nodos: Ejecutors│ -│ Aristas: Depend.│ -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│PipelineExecutor │ ← Ejecuta el DAG resolviendo dependencias -│ │ -│ execute() │ Características: -│ │ • Ejecución asíncrona (asyncio) -│ │ • Resolución de dependencias -│ │ • Evaluación de condiciones -│ │ • Manejo de errores por ejecutor -│ │ • Eventos de ejecución -│ │ • Estadísticas de rendimiento -└────────┬────────┘ - │ - ▼ -┌─────────────────┐ -│ PipelineResult │ ← Resultado final de la ejecución -│ │ -│ status: Success │ -│ outputs: {...} │ -│ events: [...] │ -│ duration: 45.2s │ -└─────────────────┘ -``` - -### 5.2 Jerarquía de clases de ejecutores - -``` -Executor (Agent Framework) - │ - └── BaseExecutor (ABC) ─── contentflow/executors/base.py - │ - │ Propiedades comunes: - │ ├── id: str - │ ├── settings: Dict - │ ├── enabled: bool - │ ├── condition: str (evaluación condicional) - │ ├── fail_pipeline_on_error: bool - │ └── debug_mode: bool - │ - │ Métodos: - │ ├── get_setting(key, default) - │ ├── resolve_env_vars(value) - │ ├── _evaluate_condition(content) - │ └── process_input(input, ctx) → Content | List[Content] [ABSTRACTO] - │ - ├── InputExecutor (ABC) ─── contentflow/executors/input_executor.py - │ │ - │ │ Propiedades adicionales: - │ │ ├── polling_interval_seconds: int (300) - │ │ ├── max_results: int (1000) - │ │ └── batch_size: int (100) - │ │ - │ │ Métodos: - │ │ ├── crawl(checkpoint) → (List[Content], token) [ABSTRACTO] - │ │ └── crawl_all(checkpoint) → List[Content] - │ │ - │ └── Implementaciones: - │ └── AzureBlobInputDiscoveryExecutor - │ - └── Ejecutores concretos (35+): - ├── Extracción - ├── Procesamiento IA - ├── Transformación - ├── Salida - ├── Enrutamiento - └── Especializados -``` - -### 5.3 Catálogo completo de ejecutores - -#### Entrada y recuperación de contenido - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `azure_blob_input_discovery` | Azure Blob Input Discovery | Descubre archivos en contenedores Blob Storage | -| `azure_blob_content_retriever` | Azure Blob Content Retriever | Recupera el contenido de blobs descubiertos | -| `content_retriever` | Content Retriever | Recuperador genérico de contenido | - -#### Extracción de documentos - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `pdf_extractor` | PDF Extractor | Extrae texto de documentos PDF | -| `word_extractor` | Word Extractor | Extrae texto de documentos Word (.docx) | -| `excel_extractor` | Excel Extractor | Extrae datos de hojas de cálculo Excel | -| `powerpoint_extractor` | PowerPoint Extractor | Extrae texto de presentaciones PowerPoint | -| `csv_extractor` | CSV Extractor | Extrae datos de archivos CSV | -| `azure_document_intelligence_extractor` | Azure Document Intelligence | Extracción inteligente con OCR y modelos pre-entrenados | -| `azure_content_understanding_extractor` | Azure Content Understanding | Análisis avanzado de contenido con AI | - -#### Procesamiento con IA - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `azure_openai_agent` | Azure OpenAI Agent | Procesamiento con modelos GPT (análisis, generación) | -| `azure_openai_embeddings` | Azure OpenAI Embeddings | Genera vectores de embedding para búsqueda semántica | -| `text_summarizer` | Text Summarizer | Genera resúmenes de texto usando IA | -| `entity_extractor` | Entity Extractor | Extrae entidades nombradas (personas, organizaciones, lugares) | -| `sentiment_analyser` | Sentiment Analyser | Análisis de sentimiento (positivo, negativo, neutro) | -| `content_classifier` | Content Classifier | Clasifica contenido en categorías definidas | -| `pii_detector` | PII Detector | Detecta información personal identificable | -| `keyword_extractor` | Keyword Extractor | Extrae palabras clave y frases importantes | -| `language_detector` | Language Detector | Detecta el idioma del contenido | -| `content_translator` | Content Translator | Traduce contenido entre idiomas | - -#### Transformación de datos - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `recursive_text_chunker` | Recursive Text Chunker | Divide texto en fragmentos con solapamiento configurable | -| `table_row_splitter` | Table Row Splitter | Divide tablas en filas individuales para procesamiento | -| `field_mapper` | Field Mapper | Mapea y transforma campos entre el modelo de contenido | -| `field_selector` | Field Selector | Selecciona campos específicos del contenido | -| `fan_in_aggregator` | Fan-In Aggregator | Agrega resultados de procesamiento paralelo | - -#### Salida y almacenamiento - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `azure_blob_output` | Azure Blob Output | Escribe contenido procesado a Blob Storage | -| `ai_search_index_output` | AI Search Index Output | Indexa contenido en Azure AI Search | -| `gptrag_search_index_document_generator` | GPT RAG Index Generator | Genera documentos para índices RAG | - -#### Enrutamiento y orquestación - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `subpipeline` | Sub-Pipeline | Ejecuta un sub-pipeline anidado | -| `for_each_content` | For Each Content | Itera sobre una lista de contenidos | -| `pass_through` | Pass Through | Pasa contenido sin modificar (debug/testing) | - -#### Conjuntos de documentos - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `document_set_initializer` | Document Set Initializer | Inicializa un conjunto de documentos para procesamiento cruzado | -| `document_set_collector` | Document Set Collector | Recopila documentos en un conjunto | -| `cross_document_comparison` | Cross-Document Comparison | Compara contenido entre múltiples documentos | -| `cross_document_field_aggregator` | Cross-Document Field Aggregator | Agrega campos de múltiples documentos | - -#### Especializados - -| ID | Nombre | Descripción | -|----|--------|-------------| -| `web_scraper` | Web Scraper | Extrae contenido de páginas web (Playwright) | -| `cosmos_db_lookup` | Cosmos DB Lookup | Consulta datos en Azure Cosmos DB | - -### 5.4 Modelo de contenido (Content) - -El modelo `Content` es la estructura de datos central que fluye por los pipelines: - -``` -Content -│ -├── Identificación -│ ├── id: str (UUID único) -│ └── source: str (origen: blob://container/path/file.pdf) -│ -├── Datos extraídos -│ ├── content: str (texto completo extraído) -│ ├── content_type: str (MIME type) -│ └── content_hash: str (hash para detección de cambios) -│ -├── Metadatos -│ ├── metadata: Dict[str, Any] -│ │ ├── filename, size, last_modified -│ │ ├── page_count, language -│ │ └── custom fields... -│ │ -│ └── tags: List[str] -│ -├── Fragmentos y embeddings -│ ├── chunks: List[Dict] (fragmentos de texto) -│ └── embeddings: List[float] (vectores numéricos) -│ -├── Resultados de análisis -│ ├── analysis: Dict[str, Any] -│ │ ├── entities, sentiment, keywords -│ │ ├── summary, classification -│ │ └── pii_detected, language -│ │ -│ └── executor_outputs: Dict[str, Any] -│ └── {executor_id: output_data} -│ -└── Registro de ejecución - └── log_entries: List[ExecutorLogEntry] - └── {executor_id, timestamp, status, duration_ms} -``` - -### 5.5 Conectores de Azure - -``` -contentflow/connectors/ -│ -├── Azure Blob Storage Connector -│ ├── Listar blobs en contenedores -│ ├── Descargar contenido de blobs -│ ├── Subir contenido a blobs -│ └── Gestionar metadatos de blobs -│ -├── Azure AI Search Connector -│ ├── Crear/actualizar índices -│ ├── Indexar documentos -│ └── Búsqueda híbrida (texto + vectores) -│ -├── Azure Document Intelligence Connector -│ ├── Analizar documentos (OCR) -│ ├── Modelos pre-entrenados (facturas, recibos, etc.) -│ └── Modelos personalizados -│ -├── Azure OpenAI Connector -│ ├── Completions (GPT-4.1, GPT-4.1-mini) -│ ├── Embeddings (text-embedding-3-small) -│ └── Chat completions -│ -└── Azure Cosmos DB Connector - ├── Consultas SQL NoSQL - ├── CRUD de documentos - └── Bulk operations -``` - -### 5.6 Diagrama del motor de pipelines - -``` -Definición YAML del Pipeline -┌──────────────────────────────────────────────────┐ -│ name: procesar-documentos │ -│ steps: │ -│ - executor: blob_input ─────────┐ │ -│ - executor: pdf_extractor ──────┐ │ │ -│ - executor: chunker ─────────┐ │ │ │ -│ - executor: embeddings ────┐ │ │ │ │ -│ - executor: blob_output ─┐ │ │ │ │ │ -└────────────────────────────┼─┼─┼──┼──┼────────────┘ - │ │ │ │ │ - PipelineFactory.parse() - │ - ▼ - ┌─ DAG de ejecución ────────────┐ - │ │ - │ [blob_input] │ - │ │ │ - │ ▼ │ - │ [pdf_extractor] │ - │ │ │ - │ ▼ │ - │ [chunker] │ - │ │ │ - │ ▼ │ - │ [embeddings] │ - │ │ │ - │ ▼ │ - │ [blob_output] │ - │ │ - └────────────────────────────────┘ - │ - PipelineExecutor.execute() - │ - ▼ - ┌─ Ejecución paso a paso ───────┐ - │ │ - │ Content ──▶ blob_input │ - │ Content ──▶ pdf_extractor │ - │ Content ──▶ chunker │ - │ Content ──▶ embeddings │ - │ Content ──▶ blob_output │ - │ │ - │ Cada paso: │ - │ 1. Evaluar condición │ - │ 2. Si enabled: ejecutar │ - │ 3. Capturar resultado/error │ - │ 4. Emitir evento │ - │ 5. Pasar al siguiente │ - │ │ - └────────────────────────────────┘ - │ - ▼ - PipelineResult - (status, outputs, - events, duration) -``` - ---- - -## 6. Infraestructura de Azure (infra) - -### 6.1 Recursos desplegados - -ContentFlow despliega los siguientes recursos de Azure: - -``` -Resource Group -│ -├── User-Assigned Managed Identity (id-${token}) -│ -├── Log Analytics Workspace (log-${token}) -│ └── Recolección de logs de todos los servicios -│ -├── Application Insights (appi-${token}) -│ └── Monitoreo, métricas y trazas distribuidas -│ -├── Cosmos DB Account (cosmos-${token}) -│ └── Database: contentflow -│ ├── executor_catalog -│ ├── pipelines -│ ├── vaults -│ ├── vault_executions -│ ├── vault_exec_locks -│ ├── vault_crawl_checkpoints -│ └── pipeline_executions -│ -├── Storage Account (st${token}) -│ ├── Blob Container: content -│ └── Queue: contentflow-execution-requests -│ -├── Container Registry (cr${token}) -│ └── Imágenes Docker de los 3 servicios -│ -├── App Configuration Store (appcs-${token}) -│ └── 25+ claves de configuración -│ -├── Container Apps Environment (cae-${token}) -│ ├── Container App: api-${token} (Puerto 8090) -│ ├── Container App: worker-${token} (Puerto 8099) -│ └── Container App: web-${token} -│ -└── AI Foundry (opcional) - ├── AI Services (S0) - └── Deployments: - ├── gpt-4.1-mini (GlobalStandard, Cap: 100) - └── gpt-4.1 (GlobalStandard, Cap: 100) -``` - -### 6.2 SKUs y niveles de servicio - -| Recurso | SKU / Nivel | Detalles | -|---------|------------|----------| -| **Cosmos DB** | Serverless | Pago por consumo, sin capacidad reservada | -| **Storage Account** | Standard_LRS | Locally Redundant Storage, Hot tier | -| **Container Registry** | Standard | Premium si se usan endpoints privados | -| **App Configuration** | Standard | Configuración centralizada | -| **Log Analytics** | PerGB2018 | Pago por GB ingerido | -| **Application Insights** | Workspace-based | Conectado a Log Analytics | -| **AI Foundry / AI Services** | S0 | Tier estándar de servicios cognitivos | -| **GPT-4.1** | GlobalStandard | Capacidad: 100 TPM | -| **GPT-4.1-mini** | GlobalStandard | Capacidad: 100 TPM | -| **Container Apps** | Consumption | Serverless, perfil de carga de trabajo por consumo | -| **Managed Identity** | User-Assigned | N/A (sin SKU, es un recurso de identidad) | - -### 6.3 Módulos Bicep - -| Módulo | Archivo | Azure Verified Module (AVM) | Versión AVM | -|--------|---------|----------------------------|-------------| -| Identidad administrada | `user-assigned-identity.bicep` | `avm/res/managed-identity/user-assigned-identity` | 0.4.1 | -| Log Analytics | `log-analytics-ws.bicep` | Built-in | — | -| Application Insights | `app-insights.bicep` | Built-in | — | -| Cosmos DB | `cosmos.bicep` | `avm/res/document-db/database-account` | 0.16.0 | -| Storage Account | `storage.bicep` | `avm/res/storage/storage-account` | 0.27.1 | -| Container Registry | `container-registry.bicep` | `avm/res/container-registry` | Built-in | -| Container Apps Environment | `container-apps-environment.bicep` | `avm/res/app/managed-environment` | 0.11.3 | -| Container App | `container-app.bicep` | `avm/res/app/container-app` | 0.19.0 | -| App Configuration | `app-config-store.bicep` | Built-in | — | -| AI Foundry | `ai-foundry.bicep` | `avm/ptn/ai-ml/ai-foundry` | 0.5.0 | -| Static Web App | `static-web-app.bicep` | Built-in (no activo) | — | - -### 6.4 Cosmos DB — Estructura de base de datos - -``` -Cosmos DB Account (cosmos-${token}) -│ -├── Tipo: NoSQL (SQL API) -├── Consistencia: Session -├── Replicación: Región única (automaticFailover: false) -├── Backup: Continuous7Days (hasta 30 días de retención) -├── Autenticación: Solo Managed Identity (contraseñas deshabilitadas) -│ -└── Database: "contentflow" - │ - ├── executor_catalog (/id) - │ └── Definiciones de ejecutores del catálogo - │ - ├── pipelines (/id) - │ └── Configuración y YAML de cada pipeline - │ - ├── vaults (/id) - │ └── Definiciones de bóvedas de documentos - │ - ├── vault_executions (/id) - │ └── Registro de ejecuciones por vault - │ - ├── vault_exec_locks (/id) - │ └── Bloqueos distribuidos para evitar ejecuciones duplicadas - │ - ├── vault_crawl_checkpoints (/id) - │ └── Puntos de control para rastreo incremental - │ - └── pipeline_executions (/id) - └── Historial detallado de ejecuciones de pipelines -``` - -### 6.5 Storage Account — Blobs y colas - -``` -Storage Account (st${token}) -│ -├── Tipo: StorageV2 -├── SKU: Standard_LRS -├── Acceso: Hot tier -├── Autenticación: Solo Managed Identity (sharedKeyAccess: false) -├── Retención de eliminados: 7 días (blobs y contenedores) -│ -├── Blob Containers: -│ └── "content" (acceso público: None) -│ ├── Documentos originales -│ └── Contenido procesado -│ -└── Queues: - └── "contentflow-execution-requests" - └── Mensajes de tareas (InputSource y ContentProcessing) -``` - -### 6.6 Container Apps — Configuración de servicios - -| Configuración | API | Worker | Web | -|---------------|-----|--------|-----| -| **Puerto** | 8090 | 8099 | HTTP estándar | -| **CPU** | 1 core | 1 core | 1 core | -| **Memoria** | 2 GiB | 2 GiB | 2 GiB | -| **Réplicas mínimas** | 1 | 1 | 1 | -| **Réplicas máximas** | 2 | 2 | 2 | -| **Ingress** | Externo | Externo | Externo | -| **Escalado** | HTTP (10 concurrentes) | HTTP (10 concurrentes) | HTTP (10 concurrentes) | -| **Health check** | `/health` (60s intervalo) | — | — | -| **CORS** | GET, POST, PUT, DELETE, OPTIONS, PATCH | GET, POST, PUT, DELETE, OPTIONS, PATCH | GET, POST, PUT, DELETE, OPTIONS, PATCH | -| **Perfil** | Consumption | Consumption | Consumption | - -### 6.7 AI Foundry — Modelos de IA - -``` -AI Foundry -│ -├── AI Services Account (S0) -│ └── Nombre del proyecto: "contentflow-project" -│ -└── Model Deployments: - │ - ├── gpt-4.1-mini - │ ├── Formato: OpenAI - │ ├── Versión: 2025-04-14 - │ ├── SKU: GlobalStandard - │ └── Capacidad: 100 (TPM) - │ - └── gpt-4.1 - ├── Formato: OpenAI - ├── Versión: 2025-04-14 - ├── SKU: GlobalStandard - └── Capacidad: 100 (TPM) -``` - -### 6.8 Diagrama de infraestructura Azure - -``` -┌──────────────────────────────────────────────────────────────────────┐ -│ RESOURCE GROUP │ -│ │ -│ ┌─ Identidad ─────────────┐ ┌─ Monitoreo ────────────────────┐ │ -│ │ │ │ │ │ -│ │ Managed Identity │ │ Log Analytics App Insights │ │ -│ │ (User-Assigned) │ │ (PerGB2018) (Workspace) │ │ -│ │ │ │ │ │ -│ │ RBAC Roles: │ └─────────────────────────────────┘ │ -│ │ • Blob Data Contributor │ │ -│ │ • Queue Data Contributor│ ┌─ Configuración ────────────────┐ │ -│ │ • Cosmos DB Data Contrib│ │ │ │ -│ └──────────────────────────┘ │ App Configuration (Standard) │ │ -│ │ 25+ claves de configuración │ │ -│ ┌─ Cómputo ───────────────────┐│ │ │ -│ │ ││ Claves: │ │ -│ │ Container Apps Environment ││ • COSMOS_DB_ENDPOINT │ │ -│ │ (Consumption workload) ││ • BLOB_STORAGE_ACCOUNT_NAME │ │ -│ │ ││ • STORAGE_WORKER_QUEUE_URL │ │ -│ │ ┌──────┐ ┌──────┐ ┌──────┐ ││ • ... │ │ -│ │ │ API │ │Worker│ │ Web │ │└─────────────────────────────────┘ │ -│ │ │ 8090 │ │ 8099 │ │ 80 │ │ │ -│ │ │ 1CPU │ │ 1CPU │ │ 1CPU │ │ ┌─ IA ────────────────────────┐ │ -│ │ │ 2GiB │ │ 2GiB │ │ 2GiB │ │ │ │ │ -│ │ └──────┘ └──────┘ └──────┘ │ │ AI Foundry (S0) │ │ -│ │ │ │ ├── gpt-4.1-mini (100 TPM)│ │ -│ │ Container Registry (Std) │ │ └── gpt-4.1 (100 TPM) │ │ -│ │ (Imágenes Docker) │ │ │ │ -│ └──────────────────────────────┘ └─────────────────────────────┘ │ -│ │ -│ ┌─ Datos ──────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ Cosmos DB (Serverless) Storage Account (Standard_LRS) │ │ -│ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │ -│ │ │ DB: contentflow │ │ Blob: "content" │ │ │ -│ │ │ ├── executor_catalog│ │ Queue: "contentflow- │ │ │ -│ │ │ ├── pipelines │ │ execution-requests" │ │ │ -│ │ │ ├── vaults │ └────────────────────────────┘ │ │ -│ │ │ ├── vault_executions│ │ │ -│ │ │ ├── vault_exec_locks│ │ │ -│ │ │ ├── vault_crawl_ │ │ │ -│ │ │ │ checkpoints │ │ │ -│ │ │ └── pipeline_ │ │ │ -│ │ │ executions │ │ │ -│ │ └─────────────────────┘ │ │ -│ └───────────────────────────────────────────────────────────────┘ │ -└──────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 7. Interconexión entre componentes - -### 7.1 Flujo de datos completo - -``` -┌─────────┐ HTTP ┌─────────┐ Write ┌──────────┐ Poll ┌──────────┐ -│ WEB │────────▶│ API │────────▶│ QUEUE │────────▶│ WORKER │ -│ (React) │ REST │(FastAPI)│ Queue │ (Azure │ Recv │ (Python) │ -└─────────┘ └────┬────┘ │ Storage) │ └────┬─────┘ - │ └──────────┘ │ - │ │ - ┌────▼────┐ ┌────▼─────┐ - │COSMOS DB│◀─────────────────────────────│ PIPELINE │ - │(Metadat)│ Lee pipelines, actualiza │ ENGINE │ - └─────────┘ estados de ejecución │ (Lib) │ - │ └────┬─────┘ - ┌────▼────┐ │ - │ BLOB │◀───────────────────────────── │ - │STORAGE │ Lee documentos, guarda │ - │(Content)│ resultados procesados │ - └─────────┘ │ - ┌────▼─────┐ - │AZURE AI │ - │SERVICES │ - │(GPT, Doc │ - │ Intell.) │ - └──────────┘ -``` - -### 7.2 Flujo de mensajes por cola - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ FLUJO DE MENSAJES POR COLA │ -│ │ -│ ① API recibe solicitud de ejecución │ -│ POST /api/pipelines/{id}/execute │ -│ │ │ -│ ▼ │ -│ ② API crea registro de ejecución en Cosmos DB │ -│ PipelineExecution { status: "pending" } │ -│ │ │ -│ ▼ │ -│ ③ API envía InputSourceTask a la cola │ -│ ┌───────────────────────────────────────────┐ │ -│ │ Queue: contentflow-execution-requests │ │ -│ │ Message: { task_type: "INPUT_SOURCE", │ │ -│ │ payload: { pipeline_id, ... } } │ │ -│ └────────────────────┬──────────────────────┘ │ -│ │ │ -│ ④ Source Worker consume la tarea │ -│ │ │ -│ ▼ │ -│ ⑤ Source Worker ejecuta ejecutores de input │ -│ → Descubre N documentos │ -│ │ │ -│ ▼ │ -│ ⑥ Source Worker crea N tareas ContentProcessingTask │ -│ ┌───────────────────────────────────────────┐ │ -│ │ Queue: contentflow-execution-requests │ │ -│ │ Message × N: │ │ -│ │ { task_type: "CONTENT_PROCESSING", │ │ -│ │ payload: { pipeline_id, content, ... } } │ │ -│ └────────────────────┬──────────────────────┘ │ -│ │ │ -│ ⑦ Processing Workers consumen tareas (en paralelo) │ -│ │ │ -│ ▼ │ -│ ⑧ Cada worker ejecuta el pipeline completo sobre un documento │ -│ (excluyendo ejecutores de input) │ -│ │ │ -│ ▼ │ -│ ⑨ Resultados almacenados en Blob Storage / Cosmos DB │ -│ PipelineExecution { status: "completed" } │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 7.3 Flujo de autenticación - -``` -┌──────────────────────────────────────────────────────────────────┐ -│ CADENA DE AUTENTICACIÓN │ -│ │ -│ User-Assigned Managed Identity │ -│ (id-${resourceToken}) │ -│ │ │ -│ ├── DefaultAzureCredential │ -│ │ (Cadena de credenciales automática) │ -│ │ │ -│ ├──▶ Cosmos DB ──── rol: "Cosmos DB SQL Data Contributor" │ -│ │ (Lectura/escritura de documentos) │ -│ │ │ -│ ├──▶ Blob Storage ─ rol: "Storage Blob Data Contributor" │ -│ │ (Lectura/escritura de blobs) │ -│ │ │ -│ ├──▶ Storage Queue ─ rol: "Storage Queue Data Contributor"│ -│ │ (Envío/recepción de mensajes) │ -│ │ │ -│ └──▶ App Config ──── rol: "App Configuration Data Reader" │ -│ (Lectura de configuración) │ -│ │ -│ Nota: Contraseñas y claves compartidas están DESHABILITADAS │ -│ (disableLocalAuthentication: true, allowSharedKeyAccess: false) │ -└──────────────────────────────────────────────────────────────────┘ -``` - -### 7.4 Matriz de comunicación entre servicios - -| Origen | Destino | Protocolo | Propósito | -|--------|---------|-----------|-----------| -| **Web** → API | HTTP/REST | CRUD de pipelines, vaults, catálogo | -| **API** → Cosmos DB | SDK Azure (HTTPS) | Almacenar/leer metadatos | -| **API** → Blob Storage | SDK Azure (HTTPS) | Gestionar documentos | -| **API** → Storage Queue | SDK Azure (HTTPS) | Encolar tareas | -| **API** → App Configuration | SDK Azure (HTTPS) | Leer configuración | -| **Worker** → Storage Queue | SDK Azure (HTTPS) | Consumir tareas | -| **Worker** → Cosmos DB | SDK Azure (HTTPS) | Leer pipelines, actualizar estados | -| **Worker** → Blob Storage | SDK Azure (HTTPS) | Leer/escribir contenido | -| **Worker** → Azure AI Services | SDK Azure (HTTPS) | Procesamiento IA | -| **Worker** → App Configuration | SDK Azure (HTTPS) | Leer configuración | -| **Todos** → App Insights | SDK Azure (HTTPS) | Telemetría y trazas | - -### 7.5 Diagrama de secuencia: Ejecución de pipeline - -``` - Usuario Web (React) API (FastAPI) Cosmos DB Storage Queue Source Worker Processing Worker Azure AI - │ │ │ │ │ │ │ │ - │ ① Crear │ │ │ │ │ │ │ - │ pipeline │ │ │ │ │ │ │ - │───────────────▶│ │ │ │ │ │ │ - │ │ POST /pipeline │ │ │ │ │ │ - │ │───────────────▶│ │ │ │ │ │ - │ │ │──── save ────▶│ │ │ │ │ - │ │ │◀──── ok ──────│ │ │ │ │ - │ │◀───── 201 ─────│ │ │ │ │ │ - │◀───────────────│ │ │ │ │ │ │ - │ │ │ │ │ │ │ │ - │ ② Ejecutar │ │ │ │ │ │ │ - │───────────────▶│ │ │ │ │ │ │ - │ │ POST /execute │ │ │ │ │ │ - │ │───────────────▶│ │ │ │ │ │ - │ │ │── save exec ─▶│ │ │ │ │ - │ │ │── queue task ─┼─────────────▶│ │ │ │ - │ │◀─── 202 ──────│ │ │ │ │ │ - │◀───────────────│ │ │ │ │ │ │ - │ │ │ │ │ │ │ │ - │ │ │ │ │ ③ poll │ │ │ - │ │ │ │ │◀─────────────│ │ │ - │ │ │ │ │── InputTask──▶ │ │ - │ │ │ │◀─── read pipeline ──────────│ │ │ - │ │ │ │──── pipeline data ─────────▶│ │ │ - │ │ │ │ │ │ │ │ - │ │ │ │ │ ④ descubrir │ │ │ - │ │ │ │ │ N documentos│ │ │ - │ │ │ │ │◀── N tasks ──│ │ │ - │ │ │ │ │ │ │ │ - │ │ │ │ │ ⑤ poll │ │ │ - │ │ │ │ │◀─────────────┼───────────────│ │ - │ │ │ │ │─ ContentTask─┼──────────────▶│ │ - │ │ │ │◀────────── read pipeline ───┼───────────────│ │ - │ │ │ │ │ │ │ - │ │ │ │ │ │ ⑥ ejecutar │ │ - │ │ │ │ │ │ pipeline │ │ - │ │ │ │ │ │──────────────▶│──── AI ─────▶│ - │ │ │ │ │ │ │◀─── result ──│ - │ │ │ │◀──────────── update status ─┼───────────────│ │ - │ │ │ │ │ │ │ │ - │ ⑦ Consultar │ │ │ │ │ │ │ - │───────────────▶│ GET /executions│ │ │ │ │ │ - │ │───────────────▶│── query ─────▶│ │ │ │ │ - │ │ │◀── results ───│ │ │ │ │ - │ │◀──── 200 ──────│ │ │ │ │ │ - │◀───────────────│ │ │ │ │ │ │ -``` - ---- - -## 8. Modos de despliegue - -### 8.1 Modo básico - -``` -┌─────────────────────────────────────────────────────────────┐ -│ MODO BÁSICO │ -│ │ -│ Ideal para: Desarrollo, pruebas, demos │ -│ │ -│ Características: │ -│ ✓ Endpoints públicos (accesibles desde internet) │ -│ ✓ Todos los recursos creados nuevos │ -│ ✓ Red virtual creada automáticamente │ -│ ✓ Log Analytics + App Insights propios │ -│ ✗ Sin endpoints privados │ -│ ✗ Sin integración con redes existentes │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ INTERNET │ │ -│ │ │ │ │ -│ │ ┌──────────────────┼───────────────────┐ │ │ -│ │ │ Container Apps Environment │ │ │ -│ │ │ │ │ │ │ -│ │ │ ┌──────┐ ┌───┴───┐ ┌──────┐ │ │ │ -│ │ │ │ Web │ │ API │ │Worker│ │ │ │ -│ │ │ │(pub) │ │(pub) │ │(pub) │ │ │ │ -│ │ │ └──────┘ └───────┘ └──────┘ │ │ │ -│ │ └──────────────────────────────────────┘ │ │ -│ │ │ │ │ -│ │ ┌──────────────────┼───────────────────┐ │ │ -│ │ │ Recursos con acceso público │ │ │ -│ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ -│ │ │ │CosmosDB│ │Storage │ │AppConf │ │ │ │ -│ │ │ │(public)│ │(public)│ │(public) │ │ │ │ -│ │ │ └────────┘ └────────┘ └────────┘ │ │ │ -│ │ └──────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -### 8.2 Modo AILZ (AI Landing Zone) - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ MODO AILZ (AI Landing Zone) │ -│ │ -│ Ideal para: Producción empresarial │ -│ │ -│ Características: │ -│ ✓ Endpoints privados para todos los datos │ -│ ✓ Integración con VNet existente │ -│ ✓ Zonas DNS privadas │ -│ ✓ Shared Log Analytics + App Insights (del Landing Zone) │ -│ ✓ Container Registry Premium (endpoints privados) │ -│ ✓ Cumplimiento de seguridad empresarial │ -│ │ -│ ┌──────────────────────────────────────────────────────────┐ │ -│ │ AI Landing Zone VNet (Existente) │ │ -│ │ │ │ -│ │ ┌─ aca-env-subnet ────────────────────────────────────┐ │ │ -│ │ │ │ │ │ -│ │ │ Container Apps Environment (internal VNet) │ │ │ -│ │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ -│ │ │ │ Web │ │ API │ │Worker│ │ │ │ -│ │ │ └──────┘ └──────┘ └──────┘ │ │ │ -│ │ │ │ │ │ -│ │ └──────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ ┌─ pe-subnet (Private Endpoints) ─────────────────────┐ │ │ -│ │ │ │ │ │ -│ │ │ ┌────────┐ ┌────────┐ ┌──────────┐ ┌────────┐ │ │ │ -│ │ │ │CosmosDB│ │Blob PE │ │Queue PE │ │ACR PE │ │ │ │ -│ │ │ │ PE │ │ │ │ │ │ │ │ │ │ -│ │ │ └────────┘ └────────┘ └──────────┘ └────────┘ │ │ │ -│ │ │ │ │ │ -│ │ └──────────────────────────────────────────────────────┘ │ │ -│ │ │ │ -│ │ Zonas DNS Privadas: │ │ -│ │ • *.documents.azure.com (Cosmos DB) │ │ -│ │ • *.blob.core.windows.net (Blob Storage) │ │ -│ │ • *.queue.core.windows.net (Storage Queue) │ │ -│ │ • *.azurecr.io (Container Registry) │ │ -│ │ • *.azconfig.io (App Configuration) │ │ -│ │ • *.cognitiveservices.azure.com (AI Services) │ │ -│ │ • Container Apps custom domain │ │ -│ └──────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ -``` - -### 8.3 Proceso de despliegue con azd - -``` -$ azd up -│ -├── ① pre-provision.sh -│ ├── Validar Azure CLI instalado -│ ├── Validar azd instalado -│ └── Validar Docker instalado -│ -├── ② Provisionar infraestructura (Bicep) -│ ├── Crear Managed Identity -│ ├── Crear Log Analytics + App Insights (básico) -│ ├── Crear Cosmos DB + containers -│ ├── Crear Storage Account + blob + queue -│ ├── Crear Container Registry -│ ├── Crear App Configuration + claves -│ ├── Crear Container Apps Environment -│ ├── Crear Container Apps (API, Worker, Web) -│ ├── Crear AI Foundry + deployments (opcional) -│ └── Asignar roles RBAC -│ -├── ③ Build y Push de imágenes Docker -│ ├── docker build contentflow-api/ -│ ├── docker build contentflow-worker/ -│ ├── docker build contentflow-web/ -│ └── docker push → Container Registry -│ -├── ④ Desplegar servicios en Container Apps -│ ├── Actualizar imagen del API -│ ├── Actualizar imagen del Worker -│ └── Actualizar imagen del Web -│ -├── ⑤ post-deploy-api.sh -│ └── Configuración post-despliegue del API -│ -└── ⑥ post-deploy.sh - └── Tareas finales interactivas - -Resultado: URLs de los servicios desplegados - → API: https://api-xxxx.azurecontainerapps.io - → Worker: https://worker-xxxx.azurecontainerapps.io - → Web: https://web-xxxx.azurecontainerapps.io -``` - -### 8.4 Diagrama comparativo de modos - -``` - MODO BÁSICO MODO AILZ - ┌─────────────────────┐ ┌─────────────────────────┐ - │ │ │ │ - │ ☁️ Internet │ │ ☁️ Internet (limitado) │ - │ │ │ │ │ │ - │ ┌────▼────┐ │ │ ┌────▼────┐ │ - │ │Container│ │ │ │Container│ │ - │ │Apps │ │ │ │Apps │ │ - │ │(público)│ │ │ │(VNet │ │ - │ └────┬────┘ │ │ │internal)│ │ - │ │ │ │ └────┬────┘ │ - │ ┌────▼───┐ │ │ ┌────▼────┐ │ - │ │Recursos│ │ │ │ Private │ │ - │ │público │ │ │ │Endpoints│ │ - │ │acceso │ │ │ │(VNet) │ │ - │ └────────┘ │ │ └─────────┘ │ - │ │ │ │ - │ ✅ Rápido │ │ ✅ Seguro │ - │ ✅ Simple │ │ ✅ Empresarial │ - │ ❌ No para prod. │ │ ❌ Requiere VNet/LZ │ - └─────────────────────┘ └─────────────────────────┘ -``` - ---- - -## 9. Seguridad y autenticación - -### 9.1 Modelo de identidad - -ContentFlow utiliza **Azure Managed Identity** (identidad administrada asignada por el usuario) para toda la autenticación entre servicios. No se utilizan contraseñas, claves de acceso ni cadenas de conexión. - -``` -┌──────────────────────────────────────────────────────┐ -│ USER-ASSIGNED MANAGED IDENTITY │ -│ (id-${resourceToken}) │ -│ │ -│ Utilizada por: │ -│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ -│ │ API │ │ Worker │ │ Web │ │ -│ │ Service │ │ Service │ │ (build) │ │ -│ └──────────┘ └──────────┘ └──────────┘ │ -│ │ -│ Autenticación en código: │ -│ DefaultAzureCredential() → cadena automática: │ -│ 1. Environment variables │ -│ 2. Managed Identity │ -│ 3. Azure CLI (desarrollo local) │ -│ 4. Azure PowerShell │ -└──────────────────────────────────────────────────────┘ -``` - -### 9.2 Roles RBAC asignados - -| Recurso | Rol RBAC | Asignado a | Permiso | -|---------|----------|-----------|---------| -| **Cosmos DB** | Cosmos DB SQL Data Contributor | Managed Identity | Leer/escribir datos | -| **Cosmos DB** | Cosmos DB SQL Data Contributor | Deployer (principal) | Leer/escribir datos | -| **Blob Storage** | Storage Blob Data Contributor | Managed Identity | Leer/escribir blobs | -| **Blob Storage** | Storage Blob Data Contributor | Deployer (principal) | Leer/escribir blobs | -| **Storage Queue** | Storage Queue Data Contributor | Managed Identity | Enviar/recibir mensajes | -| **Storage Queue** | Storage Queue Data Contributor | Deployer (principal) | Enviar/recibir mensajes | -| **App Configuration** | App Configuration Data Reader | Managed Identity | Leer configuración | -| **Container Registry** | AcrPull | Managed Identity | Descargar imágenes | - -### 9.3 Diagrama de seguridad - -``` -┌────────────────────────────────────────────────────────────────────┐ -│ MODELO DE SEGURIDAD │ -│ │ -│ ┌─ Autenticación ─────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ✅ Managed Identity (sin contraseñas) │ │ -│ │ ✅ DefaultAzureCredential (cadena automática) │ │ -│ │ ✅ disableLocalAuthentication: true (Cosmos DB) │ │ -│ │ ✅ allowSharedKeyAccess: false (Storage) │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ Autorización ──────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ✅ RBAC (Role-Based Access Control) para cada recurso │ │ -│ │ ✅ Principio de mínimo privilegio │ │ -│ │ ✅ Roles específicos por tipo de operación │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ Red (modo AILZ) ──────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ✅ Private Endpoints para datos │ │ -│ │ ✅ Subnets dedicadas │ │ -│ │ ✅ DNS privado │ │ -│ │ ✅ Network rules: Deny por defecto │ │ -│ │ ✅ Bypass: AzureServices │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─ Datos ─────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ✅ Encriptación en tránsito (HTTPS/TLS) │ │ -│ │ ✅ Encriptación en reposo (Azure managed keys) │ │ -│ │ ✅ Backup continuo (Cosmos DB: 7-30 días) │ │ -│ │ ✅ Soft delete (Blob Storage: 7 días retención) │ │ -│ │ │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -└────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 10. Configuración centralizada - -### 10.1 Azure App Configuration - -Todas las configuraciones de runtime se almacenan en **Azure App Configuration** y se leen tanto por el API como por el Worker: - -``` -App Configuration Store (appcs-${token}) -│ -├── Cosmos DB -│ ├── COSMOS_DB_ENDPOINT: https://cosmos-xxx.documents.azure.com -│ ├── COSMOS_DB_NAME: contentflow -│ ├── COSMOS_DB_CONTAINER_PIPELINES: pipelines -│ ├── COSMOS_DB_CONTAINER_VAULTS: vaults -│ ├── COSMOS_DB_CONTAINER_VAULT_EXECUTIONS: vault_executions -│ ├── COSMOS_DB_CONTAINER_VAULT_EXECUTION_LOCKS: vault_exec_locks -│ ├── COSMOS_DB_CONTAINER_CRAWL_CHECKPOINTS: vault_crawl_checkpoints -│ └── COSMOS_DB_CONTAINER_PIPELINE_EXECUTIONS: pipeline_executions -│ -├── Storage -│ ├── BLOB_STORAGE_ACCOUNT_NAME: stxxxx -│ ├── BLOB_STORAGE_CONTAINER_NAME: content -│ ├── STORAGE_ACCOUNT_WORKER_QUEUE_URL: https://stxxxx.queue.core.windows.net -│ └── STORAGE_WORKER_QUEUE_NAME: contentflow-execution-requests -│ -├── Worker -│ ├── WORKER_ENGINE_API_ENDPOINT: https://worker-xxx.azurecontainerapps.io -│ ├── NUM_PROCESSING_WORKERS: 0 -│ └── NUM_SOURCE_WORKERS: 1 -│ -└── Application - ├── API_SERVER_PORT: 8090 - ├── LOG_LEVEL: DEBUG - └── DEBUG: true -``` - -### 10.2 Variables de entorno por servicio - -| Variable | API | Worker | Descripción | -|----------|:---:|:------:|-------------| -| `COSMOS_DB_ENDPOINT` | ✅ | ✅ | Endpoint de Cosmos DB | -| `COSMOS_DB_NAME` | ✅ | ✅ | Nombre de la base de datos | -| `BLOB_STORAGE_ACCOUNT_NAME` | ✅ | ✅ | Nombre de la cuenta de storage | -| `BLOB_STORAGE_CONTAINER_NAME` | ✅ | ✅ | Nombre del contenedor blob | -| `STORAGE_ACCOUNT_WORKER_QUEUE_URL` | ✅ | ✅ | URL de la cola | -| `STORAGE_WORKER_QUEUE_NAME` | ✅ | ✅ | Nombre de la cola | -| `API_SERVER_PORT` | ✅ | — | Puerto del API (8090) | -| `API_PORT` | — | ✅ | Puerto de health del worker (8099) | -| `NUM_PROCESSING_WORKERS` | — | ✅ | Workers de procesamiento | -| `NUM_SOURCE_WORKERS` | — | ✅ | Workers de entrada | -| `QUEUE_POLL_INTERVAL_SECONDS` | — | ✅ | Intervalo de poll (5s) | -| `DEFAULT_POLLING_INTERVAL_SECONDS` | — | ✅ | Intervalo de descubrimiento (300s) | -| `MAX_TASK_RETRIES` | — | ✅ | Reintentos por tarea (3) | -| `TASK_TIMEOUT_SECONDS` | — | ✅ | Timeout por tarea (600s) | -| `LOG_LEVEL` | ✅ | ✅ | Nivel de logging (DEBUG) | -| `DEBUG` | ✅ | ✅ | Modo debug | - ---- - -## 11. Monitoreo y observabilidad - -``` -┌─────────────────────────────────────────────────────────────┐ -│ STACK DE OBSERVABILIDAD │ -│ │ -│ ┌─ Application Insights ─────────────────────────────────┐ │ -│ │ │ │ -│ │ • Trazas distribuidas entre servicios │ │ -│ │ • Métricas de rendimiento (latencia, errores) │ │ -│ │ • Logs de aplicación (Python + React) │ │ -│ │ • Mapa de dependencias entre componentes │ │ -│ │ • Alertas configurables │ │ -│ │ │ │ -│ └─────────────────────┬───────────────────────────────────┘ │ -│ │ │ -│ ┌─ Log Analytics ─────▼──────────────────────────────────┐ │ -│ │ │ │ -│ │ • Logs de Container Apps (stdout/stderr) │ │ -│ │ • Logs de sistema de Container Apps Environment │ │ -│ │ • Métricas de infraestructura │ │ -│ │ • Consultas KQL personalizables │ │ -│ │ • Retención configurable │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────┘ │ -│ │ -│ Fuentes de datos: │ -│ ├── API Service ──────▶ Logs HTTP, latencia, errores │ -│ ├── Worker Service ───▶ Logs de procesamiento, tareas │ -│ ├── Web Service ──────▶ Logs de build, errores client-side │ -│ └── Container Apps ───▶ Escalado, arranques, health checks │ -└─────────────────────────────────────────────────────────────┘ -``` - ---- - -## 12. Resumen de dependencias entre componentes - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ MAPA DE DEPENDENCIAS DE CONTENTFLOW │ -│ │ -│ │ -│ contentflow-web ──────────────▶ contentflow-api │ -│ (Frontend) HTTP/REST (Backend) │ -│ │ │ -│ ├──── azure-cosmos (SDK) │ -│ ├──── azure-storage-blob (SDK) │ -│ ├──── azure-storage-queue (SDK) │ -│ ├──── azure-appconfiguration (SDK) │ -│ └──── contentflow-lib (local) │ -│ │ │ -│ contentflow-worker ─────────────────────────────┤ │ -│ (Processing) │ │ -│ │ │ │ -│ ├──── azure-cosmos (SDK) │ │ -│ ├──── azure-storage-queue (SDK) contentflow-lib │ -│ ├──── azure-storage-blob (SDK) (Core Engine) │ -│ └──── contentflow-lib (local) ──┐ │ │ -│ │ ├── Pipeline Engine │ -│ └──────────├── 35+ Ejecutores │ -│ ├── Content Model │ -│ ├── Conectores Azure │ -│ │ ├── Blob Storage │ -│ │ ├── AI Search │ -│ │ ├── Document Intel. │ -│ │ ├── OpenAI │ -│ │ └── Cosmos DB │ -│ └── Utilidades │ -│ │ -│ infra/ (Bicep) │ -│ │ │ -│ └──── Aprovisiona TODOS los recursos de Azure que los │ -│ servicios anteriores consumen │ -│ │ -│ azure.yaml │ -│ │ │ -│ └──── Orquesta el despliegue de infra/ + servicios │ -│ via Azure Developer CLI (azd) │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -> **Este documento refleja el estado de la arquitectura a la fecha de generación. Consulte el código fuente para la información más actualizada.** diff --git a/Analysis/03-use_cases_examples.md b/Analysis/03-use_cases_examples.md deleted file mode 100644 index d2e641f..0000000 --- a/Analysis/03-use_cases_examples.md +++ /dev/null @@ -1,666 +0,0 @@ -# ContentFlow — Casos de Uso y Ejemplos - -> **Catálogo de escenarios implementables con el acelerador ContentFlow**, organizados por industria y función, con los ejecutores recomendados para cada caso. - ---- - -## Índice - -- [1. Procesamiento de facturas y recibos](#1-procesamiento-de-facturas-y-recibos) -- [2. Indexación inteligente de contenido](#2-indexación-inteligente-de-contenido) -- [3. Análisis de contratos](#3-análisis-de-contratos) -- [4. Grafos de conocimiento evolutivos](#4-grafos-de-conocimiento-evolutivos) -- [5. Procesamiento de formularios](#5-procesamiento-de-formularios) -- [6. Cumplimiento normativo y gestión de riesgos](#6-cumplimiento-normativo-y-gestión-de-riesgos) -- [7. Procesamiento de contenido web](#7-procesamiento-de-contenido-web) -- [8. Análisis de imagen y contenido visual](#8-análisis-de-imagen-y-contenido-visual) -- [9. Procesamiento de adjuntos de correo electrónico](#9-procesamiento-de-adjuntos-de-correo-electrónico) -- [10. Ingestión de contenido para GPT-RAG](#10-ingestión-de-contenido-para-gpt-rag) -- [11. Resumen de artículos y reportes](#11-resumen-de-artículos-y-reportes) -- [12. Traducción de contenido multilingüe](#12-traducción-de-contenido-multilingüe) -- [13. Resumen de ejecutores por caso de uso](#13-resumen-de-ejecutores-por-caso-de-uso) -- [14. Pipelines de ejemplo incluidos en el repositorio](#14-pipelines-de-ejemplo-incluidos-en-el-repositorio) - ---- - -## 1. Procesamiento de facturas y recibos - -Automatiza la extracción de datos estructurados de facturas, recibos y órdenes de compra usando Azure Content Understanding. - -**Capacidades**: -- Extraer datos estructurados de facturas usando Azure Content Understanding -- Clasificar documentos por tipo (factura, recibo, orden de compra) -- Validar campos extraídos y señalar anomalías -- Exportar a sistemas ERP o bases de datos - -**Pipeline sugerido**: - -```yaml -name: procesamiento-facturas -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: facturas-entrantes - file_extensions: [".pdf", ".png", ".jpg"] - - - executor: azure_blob_content_retriever - - - executor: azure_content_understanding_extractor - settings: - model_id: prebuilt-invoice - - - executor: content_classifier - settings: - categories: ["factura", "recibo", "orden_de_compra"] - - - executor: field_mapper - settings: - mappings: - vendor_name: "proveedor" - total_amount: "monto_total" - invoice_date: "fecha_factura" - - - executor: azure_blob_output - settings: - blob_container_name: facturas-procesadas -``` - -**Ejecutores clave**: `azure_content_understanding_extractor`, `content_classifier`, `field_mapper`, `azure_blob_output` - ---- - -## 2. Indexación inteligente de contenido - -Procesa documentos, imágenes, audio y video para generar embeddings vectoriales e indexar en Azure AI Search, habilitando búsqueda semántica empresarial. - -**Capacidades**: -- Procesar documentos en múltiples formatos (PDF, Word, Excel, PowerPoint) -- Generar vectores de embedding para búsqueda semántica -- Indexar contenido en Azure AI Search -- Habilitar búsqueda semántica en todo el contenido empresarial - -**Pipeline sugerido**: - -```yaml -name: indexacion-inteligente -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documentos-empresa - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - settings: - model_id: prebuilt-layout - - - executor: recursive_text_chunker - settings: - chunk_size: 1000 - chunk_overlap: 200 - - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - - executor: ai_search_index_output - settings: - index_name: contenido-empresarial -``` - -**Ejecutores clave**: `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `ai_search_index_output` - ---- - -## 3. Análisis de contratos - -Extrae cláusulas clave, identifica partes involucradas, fechas, montos, y genera resúmenes automáticos para revisión legal. - -**Capacidades**: -- Extraer cláusulas clave y obligaciones contractuales -- Identificar partes, fechas y valores monetarios -- Señalar términos riesgosos usando análisis de IA -- Generar resúmenes para revisión legal - -**Pipeline sugerido**: - -```yaml -name: analisis-contratos -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: contratos - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: entity_extractor - settings: - entity_types: ["persona", "organizacion", "fecha", "monto"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1 - prompt: | - Analiza este contrato y extrae: - 1. Cláusulas clave y obligaciones - 2. Términos riesgosos o inusuales - 3. Fechas límite y vencimientos - 4. Valores monetarios y condiciones de pago - - - executor: text_summarizer - settings: - max_length: 500 - style: "resumen ejecutivo legal" - - - executor: azure_blob_output - settings: - blob_container_name: contratos-analizados -``` - -**Ejecutores clave**: `pdf_extractor`, `entity_extractor`, `azure_openai_agent`, `text_summarizer` - ---- - -## 4. Grafos de conocimiento evolutivos - -Construye grafos de conocimiento que mapean entidades, relaciones y estructuras organizativas a partir de documentos corporativos. - -**Capacidades**: -- Extraer entidades (personas, organizaciones, productos, ubicaciones) -- Identificar relaciones (trabaja_en, gestiona, ubicado_en) -- Mapear estructuras organizativas -- Descubrir conexiones ocultas entre documentos - -**Pipeline sugerido**: - -```yaml -name: grafo-conocimiento -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documentos-corporativos - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - - - executor: recursive_text_chunker - settings: - chunk_size: 2000 - - - executor: entity_extractor - settings: - entity_types: ["persona", "organizacion", "producto", "ubicacion"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1 - prompt: | - A partir de las entidades extraídas, identifica relaciones como: - - trabaja_en, gestiona, reporta_a - - ubicado_en, opera_en - - provee, compra_a, compite_con - Devuelve las relaciones en formato estructurado. - - - executor: azure_blob_output - settings: - blob_container_name: grafos-conocimiento -``` - -**Ejecutores clave**: `entity_extractor`, `azure_openai_agent`, `document_set_initializer`, `cross_document_comparison` - ---- - -## 5. Procesamiento de formularios - -Automatiza la captura y validación de datos de formularios, solicitudes, reclamaciones y aplicaciones. - -**Capacidades**: -- Procesar solicitudes, reclamaciones y formularios -- Extraer campos estructurados con ejecutores de IA -- Validar completitud y precisión de los datos -- Enrutar al sistema de línea de negocio (LoB) apropiado - -**Pipeline sugerido**: - -```yaml -name: procesamiento-formularios -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: formularios-entrantes - - - executor: azure_blob_content_retriever - - - executor: azure_content_understanding_extractor - settings: - model_id: prebuilt-document - - - executor: field_selector - settings: - fields: ["nombre", "fecha", "tipo_solicitud", "monto", "descripcion"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1-mini - prompt: | - Valida los campos extraídos: - - ¿Están todos los campos obligatorios presentes? - - ¿Los formatos son correctos (fechas, montos)? - - Señala campos faltantes o inconsistentes. - - - executor: content_classifier - settings: - categories: ["solicitud_credito", "reclamacion_seguro", "solicitud_empleo"] - - - executor: azure_blob_output - settings: - blob_container_name: formularios-procesados -``` - -**Ejecutores clave**: `azure_content_understanding_extractor`, `field_selector`, `content_classifier`, `azure_openai_agent` - ---- - -## 6. Cumplimiento normativo y gestión de riesgos - -Detecta información personal identificable (PII), clasifica documentos por sensibilidad y señala problemas de cumplimiento. - -**Capacidades**: -- Detectar PII (Información Personalmente Identificable) -- Clasificar documentos por nivel de sensibilidad -- Señalar problemas de cumplimiento normativo -- Extraer entidades clave y temas sensibles - -**Pipeline sugerido**: - -```yaml -name: cumplimiento-riesgos -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: documentos-revision - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: pii_detector - settings: - pii_types: ["nombre", "email", "telefono", "numero_seguro_social", - "tarjeta_credito", "direccion", "fecha_nacimiento"] - - - executor: content_classifier - settings: - categories: ["publico", "interno", "confidencial", "restringido"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1 - prompt: | - Analiza el documento para cumplimiento normativo: - - ¿Contiene información regulada (GDPR, HIPAA, PCI-DSS)? - - ¿Se manejan datos sensibles correctamente? - - Identifica riesgos potenciales de cumplimiento. - Genera un reporte con nivel de riesgo: bajo, medio, alto, crítico. - - - executor: azure_blob_output - settings: - blob_container_name: reportes-cumplimiento -``` - -**Ejecutores clave**: `pii_detector`, `content_classifier`, `entity_extractor`, `azure_openai_agent` - ---- - -## 7. Procesamiento de contenido web - -Extrae, traduce y corrige contenido de sitios web para alimentar soluciones de chat basadas en RAG o sitios multilingües. - -**Capacidades**: -- Extraer contenido de sitios web para soluciones de chat basadas en RAG -- Traducir contenido web para experiencias multilingües -- Identificar y corregir errores ortográficos y de contenido - -**Pipeline sugerido**: - -```yaml -name: procesamiento-web -steps: - - executor: web_scraper - settings: - urls: - - "https://docs.empresa.com" - - "https://soporte.empresa.com" - max_depth: 3 - selectors: ["article", "main", ".content"] - - - executor: recursive_text_chunker - settings: - chunk_size: 1500 - - - executor: language_detector - - - executor: content_translator - settings: - target_language: "es" - - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - - executor: ai_search_index_output - settings: - index_name: contenido-web-rag -``` - -**Ejecutores clave**: `web_scraper`, `language_detector`, `content_translator`, `azure_openai_embeddings` - ---- - -## 8. Análisis de imagen y contenido visual - -Digitaliza notas clínicas manuscritas, extrae texto de reportes visuales y analiza imágenes de daños para procesamiento automatizado de reclamaciones. - -**Capacidades**: - -| Industria | Aplicación | -|-----------|-----------| -| **Salud** | Digitalizar notas clínicas manuscritas, extraer texto de reportes, generar descripciones de imágenes médicas para registros electrónicos de salud y cumplimiento de accesibilidad | -| **Seguros** | Procesar formularios manuscritos de reclamaciones, extraer datos de fotos de accidentes, analizar imágenes de evaluación de daños para procesamiento automatizado | - -**Pipeline sugerido**: - -```yaml -name: analisis-visual-seguros -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: reclamaciones-fotos - file_extensions: [".jpg", ".png", ".pdf"] - - - executor: azure_blob_content_retriever - - - executor: azure_content_understanding_extractor - settings: - model_id: prebuilt-document - - - executor: azure_openai_agent - settings: - model: gpt-4.1 - prompt: | - Analiza esta imagen/documento de reclamación de seguro: - - Identifica el tipo de daño visible - - Estima la severidad (leve, moderado, severo) - - Extrae información del formulario de reclamación - - Señala cualquier inconsistencia - - - executor: content_classifier - settings: - categories: ["vehicular", "propiedad", "salud", "vida"] - - - executor: azure_blob_output - settings: - blob_container_name: reclamaciones-procesadas -``` - -**Ejecutores clave**: `azure_content_understanding_extractor`, `azure_openai_agent`, `content_classifier` - ---- - -## 9. Procesamiento de adjuntos de correo electrónico - -Analiza hilos de correo para extraer tareas, detectar sentimiento y enrutar automáticamente a los equipos adecuados. - -**Capacidades**: - -| Industria | Aplicación | -|-----------|-----------| -| **Servicio al cliente** | Analizar hilos de soporte para extraer acciones pendientes, detectar sentimiento del cliente, y enrutar automáticamente problemas urgentes a los equipos apropiados | -| **Legal** | Categorizar automáticamente correspondencia legal, identificar puntos de negociación, extraer fechas límite y obligaciones de hilos de correo | - -**Pipeline sugerido**: - -```yaml -name: procesamiento-correos -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: correos-entrantes - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: sentiment_analyser - settings: - granularity: "documento" - - - executor: entity_extractor - settings: - entity_types: ["persona", "fecha", "organizacion", "accion"] - - - executor: azure_openai_agent - settings: - model: gpt-4.1-mini - prompt: | - Del correo electrónico, extrae: - 1. Acciones pendientes y responsables - 2. Fechas límite mencionadas - 3. Nivel de urgencia (bajo, medio, alto, crítico) - 4. Tema principal y categoría - - - executor: content_classifier - settings: - categories: ["soporte_tecnico", "facturacion", "queja", - "solicitud_informacion", "legal"] - - - executor: azure_blob_output - settings: - blob_container_name: correos-procesados -``` - -**Ejecutores clave**: `sentiment_analyser`, `entity_extractor`, `azure_openai_agent`, `content_classifier` - ---- - -## 10. Ingestión de contenido para GPT-RAG - -Construye bases de conocimiento buscables a partir de documentos empresariales, procesando múltiples formatos y creando embeddings para búsqueda semántica en soluciones de chat con IA. - -**Capacidades**: -- Construir bases de conocimiento buscables desde documentos empresariales -- Procesar contenido en múltiples formatos para chat impulsado por IA -- Crear embeddings para búsqueda semántica - -**Pipeline sugerido**: - -```yaml -name: ingestion-gpt-rag -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: base-conocimiento - - - executor: azure_blob_content_retriever - - - executor: azure_document_intelligence_extractor - settings: - model_id: prebuilt-layout - - - executor: recursive_text_chunker - settings: - chunk_size: 800 - chunk_overlap: 150 - - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - - executor: gptrag_search_index_document_generator - settings: - index_name: rag-knowledge-base - - - executor: ai_search_index_output - settings: - index_name: rag-knowledge-base -``` - -**Ejecutores clave**: `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `gptrag_search_index_document_generator` - ---- - -## 11. Resumen de artículos y reportes - -Genera resúmenes concisos de artículos de noticias, reportes ejecutivos y snippets para redes sociales. - -**Capacidades**: -- Resumir artículos de noticias para boletines internos -- Crear resúmenes ejecutivos de reportes extensos -- Generar snippets para publicaciones en redes sociales - -**Pipeline sugerido**: - -```yaml -name: resumen-articulos -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: articulos-reportes - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: language_detector - - - executor: keyword_extractor - settings: - max_keywords: 10 - - - executor: text_summarizer - settings: - max_length: 300 - style: "resumen ejecutivo" - - - executor: azure_openai_agent - settings: - model: gpt-4.1-mini - prompt: | - A partir del resumen generado, crea: - 1. Un titular de una línea - 2. Un resumen de 3 oraciones para boletín - 3. Un snippet para redes sociales (max 280 caracteres) - - - executor: azure_blob_output - settings: - blob_container_name: resumenes-generados -``` - -**Ejecutores clave**: `keyword_extractor`, `text_summarizer`, `language_detector`, `azure_openai_agent` - ---- - -## 12. Traducción de contenido multilingüe - -Traduce documentación de productos, marketing y bases de conocimiento para alcanzar audiencias globales. - -**Capacidades**: -- Traducir documentación de productos a múltiples idiomas -- Localizar contenido de marketing para diferentes regiones -- Crear bases de conocimiento multilingües - -**Pipeline sugerido**: - -```yaml -name: traduccion-multilingue -steps: - - executor: azure_blob_input_discovery - settings: - blob_container_name: contenido-original - - - executor: azure_blob_content_retriever - - - executor: pdf_extractor - - - executor: language_detector - - - executor: recursive_text_chunker - settings: - chunk_size: 2000 - - - executor: content_translator - settings: - target_languages: ["es", "fr", "de", "pt", "ja"] - - - executor: azure_openai_embeddings - settings: - model: text-embedding-3-small - - - executor: ai_search_index_output - settings: - index_name: contenido-multilingue -``` - -**Ejecutores clave**: `language_detector`, `content_translator`, `azure_openai_embeddings`, `ai_search_index_output` - ---- - -## 13. Resumen de ejecutores por caso de uso - -| Caso de uso | Ejecutores principales | -|---|---| -| **Facturas y recibos** | `azure_content_understanding_extractor`, `content_classifier`, `field_mapper` | -| **Indexación inteligente** | `azure_document_intelligence_extractor`, `recursive_text_chunker`, `azure_openai_embeddings`, `ai_search_index_output` | -| **Análisis de contratos** | `pdf_extractor`, `entity_extractor`, `azure_openai_agent`, `text_summarizer` | -| **Grafos de conocimiento** | `entity_extractor`, `azure_openai_agent`, `cross_document_comparison` | -| **Formularios** | `azure_content_understanding_extractor`, `field_selector`, `content_classifier` | -| **Cumplimiento y riesgos** | `pii_detector`, `content_classifier`, `azure_openai_agent` | -| **Contenido web** | `web_scraper`, `content_translator`, `azure_openai_embeddings` | -| **Análisis visual** | `azure_content_understanding_extractor`, `azure_openai_agent` | -| **Correo electrónico** | `sentiment_analyser`, `entity_extractor`, `content_classifier` | -| **GPT-RAG** | `azure_document_intelligence_extractor`, `recursive_text_chunker`, `gptrag_search_index_document_generator` | -| **Resúmenes** | `keyword_extractor`, `text_summarizer`, `azure_openai_agent` | -| **Traducción** | `language_detector`, `content_translator`, `azure_openai_embeddings` | - ---- - -## 14. Pipelines de ejemplo incluidos en el repositorio - -El repositorio incluye más de **20 pipelines de ejemplo** listos para ejecutar en la carpeta `contentflow-lib/samples/`: - -| Carpeta | Nombre | Descripción | -|---------|--------|-------------| -| `01-simple/` | Pipeline simple | Pipeline básico de extracción de texto | -| `02-batch-processing/` | Procesamiento por lotes | Procesamiento de múltiples documentos | -| `03-pdf-extractor_chunker/` | PDF + Chunking | Extracción de PDF y división en fragmentos | -| `04-word-extractor/` | Extractor Word | Procesamiento de documentos .docx | -| `05-powerpoint-extractor/` | Extractor PowerPoint | Procesamiento de presentaciones .pptx | -| `06-ai-analysis/` | Análisis con IA | Análisis inteligente con GPT | -| `07-embeddings/` | Embeddings | Generación de vectores de embedding | -| `08-content-understanding/` | Content Understanding | Azure Content Understanding | -| `09-blob-input/` | Entrada desde Blob | Descubrimiento de contenido en Blob Storage | -| `10-table-row-splitter/` | Divisor de filas | Procesamiento fila por fila de tablas | -| `11-excel-extractor/` | Extractor Excel | Procesamiento de hojas de cálculo | -| `12-field-transformation/` | Transformación de campos | Mapeo y transformación de datos | -| `13-blob-output-sample/` | Salida a Blob | Escritura de resultados a Blob Storage | -| `14-gpt-rag-ingestion/` | Ingestión GPT-RAG | Pipeline completo para RAG | -| `15-document-analysis/` | Análisis de documentos | Análisis completo con Document Intelligence | -| `16-spreadsheet-pipeline/` | Pipeline de hojas de cálculo | Procesamiento de Excel end-to-end | -| `17-knowledge-graph/` | Grafo de conocimiento | Extracción de entidades y relaciones | -| `18-web-scraping/` | Web scraping | Extracción de contenido web | -| `19-sub-pipelines/` | Sub-pipelines | Pipelines anidados | -| `20-document-set-static/` | Conjunto estático | Procesamiento de conjuntos de documentos | -| `21-document-set-comparison/` | Comparación de documentos | Comparación cruzada de documentos | -| `22-document-set-dynamic/` | Conjunto dinámico | Conjuntos de documentos dinámicos | -| `23-inline-document-set/` | Conjunto inline | Conjuntos definidos en línea | -| `27-subpipeline-processing/` | Sub-pipeline avanzado | Procesamiento con sub-pipelines | -| `28-advanced-batch/` | Lotes avanzado | Procesamiento por lotes avanzado | -| `32-parallel-processing/` | Procesamiento paralelo | Flujos de trabajo en paralelo | -| `44-conditional-routing/` | Enrutamiento condicional | Lógica condicional en pipelines | - ---- - -> Cada caso de uso se puede implementar combinando los **35+ ejecutores** disponibles en ContentFlow. Los pipelines YAML son completamente configurables y se pueden diseñar visualmente desde la interfaz web o editarse directamente en el editor YAML integrado. diff --git a/Analysis/04-ai_lz_options.md b/Analysis/04-ai_lz_options.md deleted file mode 100644 index 07aeb4d..0000000 --- a/Analysis/04-ai_lz_options.md +++ /dev/null @@ -1,1224 +0,0 @@ -# Guía de Despliegue: ContentFlow en Modo AI Landing Zone Integrated - -> **Documento técnico operativo** - Instrucciones paso a paso para desplegar ContentFlow con conectividad privada usando una AI Landing Zone mínima en un ambiente de demo con suscripción única. - ---- - -## Tabla de Contenidos - -1. [Resumen Ejecutivo](#1-resumen-ejecutivo) -2. [¿Qué es el Modo AILZ-Integrated?](#2-qué-es-el-modo-ailz-integrated) -3. [Arquitectura de Red: Basic vs AILZ](#3-arquitectura-de-red-basic-vs-ailz) -4. [Prerrequisitos Generales](#4-prerrequisitos-generales) -5. [El Problema: Demo con Suscripción Única](#5-el-problema-demo-con-suscripción-única) -6. [Solución: AI Landing Zone Mínima ("Mini-AILZ")](#6-solución-ai-landing-zone-mínima-mini-ailz) -7. [Paso a Paso: Crear la Mini-AILZ](#7-paso-a-paso-crear-la-mini-ailz) -8. [Paso a Paso: Desplegar ContentFlow en Modo AILZ](#8-paso-a-paso-desplegar-contentflow-en-modo-ailz) -9. [Acceso y Pruebas de Conectividad Privada](#9-acceso-y-pruebas-de-conectividad-privada) -10. [Mapa Completo de Parámetros](#10-mapa-completo-de-parámetros) -11. [Cambios por Recurso en Modo AILZ](#11-cambios-por-recurso-en-modo-ailz) -12. [Troubleshooting](#12-troubleshooting) -13. [Costos Estimados de la Mini-AILZ](#13-costos-estimados-de-la-mini-ailz) -14. [Limpieza de Recursos](#14-limpieza-de-recursos) - ---- - -## 1. Resumen Ejecutivo - -ContentFlow soporta dos modos de despliegue: - -| Aspecto | `basic` | `ailz-integrated` | -|---|---|---| -| Endpoints | Públicos (internet) | Privados (VNet) | -| Red | Sin VNet | VNet + Private Endpoints | -| DNS | Resolución pública | Private DNS Zones | -| ACR SKU | Standard | **Premium** (requerido para PE) | -| Firewall de recursos | `Allow` (todo abierto) | `Deny` (solo PE) | -| Ingress de Container Apps | Externo (público) | **Interno** (solo VNet) | -| Acceso a la UI Web | Navegador directo | Requiere VPN/JumpBox | - -**Escenario de este documento:** Tienes **una sola suscripción de Azure** para demos y quieres probar el modo `ailz-integrated` completo. Normalmente, la AI Landing Zone ya existe como infraestructura compartida enterprise. Aquí crearemos una **versión mínima ("Mini-AILZ")** en la misma suscripción para simular ese escenario. - ---- - -## 2. ¿Qué es el Modo AILZ-Integrated? - -En un entorno enterprise real, el **AI Landing Zone** es una infraestructura de red pre-provisionada por el equipo de plataforma que incluye: - -- **Virtual Network (VNet)** con subredes dedicadas -- **Private DNS Zones** para resolución de nombres internos -- **Network Security Groups (NSGs)** y políticas de seguridad -- **JumpBox VM** para acceso administrativo -- **Shared Log Analytics** y **Application Insights** centralizados -- **Azure Firewall** o NVA para control de tráfico (enterprise completo) - -ContentFlow en modo `ailz-integrated` **no crea red propia**. En su lugar: - -1. **Reutiliza** la VNet y subredes existentes del AILZ -2. **Crea Private Endpoints** en la subred dedicada (`pe-subnet`) -3. **Registra registros DNS** en las Private DNS Zones existentes -4. **Configura todos los recursos** con `publicNetworkAccess: Disabled` -5. **Integra Container Apps Environment** con la VNet (modo interno) - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ AI Landing Zone (VNet) │ -│ │ -│ ┌──────────────────────┐ ┌────────────────────────────────────┐ │ -│ │ pe-subnet (/27+) │ │ aca-env-subnet (/23) │ │ -│ │ │ │ │ │ -│ │ ● Storage PE (blob) │ │ ┌──────────────────────────────┐ │ │ -│ │ ● Storage PE (queue)│ │ │ Container Apps Environment │ │ │ -│ │ ● Cosmos DB PE │ │ │ (Internal Load Balancer) │ │ │ -│ │ ● App Config PE │ │ │ │ │ │ -│ │ ● ACR PE (Premium) │ │ │ ┌─────┐ ┌──────┐ ┌─────┐ │ │ │ -│ │ ● AI Foundry PE │ │ │ │ API │ │Worker│ │ Web │ │ │ │ -│ │ │ │ │ └─────┘ └──────┘ └─────┘ │ │ │ -│ └──────────────────────┘ │ └──────────────────────────────┘ │ │ -│ └────────────────────────────────────┘ │ -│ │ -│ ┌──────────────┐ ┌──────────────────────────────────────────────┐ │ -│ │ jumpbox-vm │ │ Private DNS Zones (6 requeridas) │ │ -│ │ (acceso) │ │ ● privatelink.blob.core.windows.net │ │ -│ └──────────────┘ │ ● privatelink.documents.azure.com │ │ -│ │ ● privatelink.azconfig.io │ │ -│ │ ● privatelink.azurecr.io │ │ -│ │ ● privatelink.cognitiveservices.azure.com │ │ -│ │ ● privatelink.azurecontainerapps.io │ │ -│ └──────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 3. Arquitectura de Red: Basic vs AILZ - -### Modo Basic (Público) - -``` -Internet ──→ Container Apps (API/Web) ──→ Storage Account (público) - ──→ Cosmos DB (público) - ──→ App Config (público) - ──→ ACR Standard (público) - ──→ AI Foundry (público) -``` - -- Todos los recursos tienen endpoints públicos -- Accesibles desde cualquier IP -- Network ACLs: `defaultAction: Allow` - -### Modo AILZ-Integrated (Privado) - -``` -JumpBox/VPN ──→ VNet ──→ Container Apps (interno) ──→ Storage PE (privado) - ──→ Cosmos PE (privado) - ──→ App Config PE (privado) - ──→ ACR PE Premium (privado) - ──→ AI Foundry PE (privado) -``` - -- Todos los recursos: `publicNetworkAccess: Disabled` -- Network ACLs: `defaultAction: Deny`, `bypass: AzureServices` -- Acceso solo vía Private Endpoints dentro de la VNet -- Container Apps: `internal: true`, solo accesible desde VNet - ---- - -## 4. Prerrequisitos Generales - -### Herramientas Requeridas - -```bash -# Verificar instalación -az --version # Azure CLI 2.60+ -azd version # Azure Developer CLI 1.5+ -docker --version # Docker Desktop (para build de containers) -``` - -### Permisos Azure Requeridos - -| Permiso | Escenario | Razón | -|---|---|---| -| `Contributor` | Resource Group de ContentFlow | Crear todos los recursos | -| `Network Contributor` | Resource Group de la VNet | Crear Private Endpoints en subredes | -| `Private DNS Zone Contributor` | Resource Group de DNS Zones | Crear registros A para PEs | -| `User Access Administrator` | Resource Group | Asignar RBAC a Managed Identity | - -> **Nota:** En el escenario Mini-AILZ (suscripción única), si usas un Resource Group separado para la red, necesitas permisos en ambos RGs. - -### Registro de Providers - -```bash -# Asegurar que estos providers estén registrados -az provider register --namespace Microsoft.App -az provider register --namespace Microsoft.ContainerRegistry -az provider register --namespace Microsoft.DocumentDB -az provider register --namespace Microsoft.Network -az provider register --namespace Microsoft.CognitiveServices -az provider register --namespace Microsoft.Storage -az provider register --namespace Microsoft.AppConfiguration -``` - ---- - -## 5. El Problema: Demo con Suscripción Única - -En un entorno enterprise real: - -``` -┌──────────────────────────────┐ ┌──────────────────────────────┐ -│ Suscripción: Platform │ │ Suscripción: Workloads │ -│ │ │ │ -│ RG: rg-ailz-network │ │ RG: rg-contentflow │ -│ ├── VNet │ │ ├── Storage + PE │ -│ ├── Private DNS Zones │ │ ├── Cosmos DB + PE │ -│ ├── NSGs │ │ ├── Container Apps (int) │ -│ ├── Azure Firewall │ │ ├── ACR Premium + PE │ -│ └── JumpBox VM │ │ ├── App Config + PE │ -│ │ │ └── AI Foundry + PE │ -│ (Manejado por Platform Team)│ │ (Manejado por App Team) │ -└──────────────────────────────┘ └──────────────────────────────┘ -``` - -**Tu situación:** Solo tienes **una suscripción**. No existe un AILZ pre-provisionado. Necesitas: - -1. **Simular** la infraestructura de red que normalmente provee el Platform Team -2. **Desplegar** ContentFlow en modo `ailz-integrated` apuntando a esa red -3. **Acceder** a los servicios internos para validar la demo - ---- - -## 6. Solución: AI Landing Zone Mínima ("Mini-AILZ") - -Crearemos la infraestructura mínima necesaria en un **Resource Group separado** dentro de la misma suscripción: - -``` -┌─────────────────────────────────────────────────────────────────────┐ -│ Suscripción Única de Demo │ -│ │ -│ ┌──────────────────────────────┐ ┌─────────────────────────────┐ │ -│ │ RG: rg-mini-ailz │ │ RG: rg-contentflow-ailz │ │ -│ │ │ │ │ │ -│ │ ● VNet (10.0.0.0/16) │ │ ● Storage + PE │ │ -│ │ ├── pe-subnet /27 │ │ ● Cosmos DB + PE │ │ -│ │ ├── aca-env-subnet /23 │ │ ● ACR Premium + PE │ │ -│ │ └── jumpbox-subnet /27 │ │ ● App Config + PE │ │ -│ │ │ │ ● Container Apps Env (int) │ │ -│ │ ● 6 Private DNS Zones │ │ ● Container Apps (API/ │ │ -│ │ (vinculadas a la VNet) │ │ Worker/Web) │ │ -│ │ │ │ ● AI Foundry + PE │ │ -│ │ ● JumpBox VM (B2s) │ │ ● App Insights (nuevo) │ │ -│ │ (con Bastion o IP pública │ │ ● Log Analytics (nuevo) │ │ -│ │ temporal para demo) │ │ ● User Assigned Identity │ │ -│ │ │ │ │ │ -│ │ ● Log Analytics (opcional) │ │ │ │ -│ │ ● App Insights (opcional) │ │ │ │ -│ └──────────────────────────────┘ └─────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────┘ -``` - -### ¿Por qué un RG separado? - -- **Simula el escenario real**: Red y aplicación en RGs diferentes -- **Limpieza fácil**: Puedes borrar `rg-contentflow-ailz` sin afectar la red -- **Permisos realistas**: Puedes validar que los permisos cross-RG funcionan -- **Reutilizable**: La Mini-AILZ puede servir para otras demos de servicios con Private Endpoints - ---- - -## 7. Paso a Paso: Crear la Mini-AILZ - -### 7.1 Definir Variables - -```bash -# === CONFIGURACIÓN === -# Ajusta estos valores a tu ambiente -SUBSCRIPTION_ID=$(az account show --query id -o tsv) -LOCATION="eastus2" # Tu región preferida -AILZ_RG="rg-mini-ailz" # RG para la infraestructura de red -CF_RG="rg-contentflow-ailz" # RG para ContentFlow (lo crea azd) -VNET_NAME="vnet-ailz-demo" -VNET_PREFIX="10.0.0.0/16" - -# Subredes -PE_SUBNET_NAME="pe-subnet" # Nombre esperado por ContentFlow -PE_SUBNET_PREFIX="10.0.1.0/27" # /27 = 32 IPs (suficiente para ~25 PEs) -ACA_SUBNET_NAME="aca-env-subnet" # Nombre esperado por ContentFlow -ACA_SUBNET_PREFIX="10.0.16.0/23" # /23 = 512 IPs (requerido por Container Apps) -JUMPBOX_SUBNET_NAME="jumpbox-subnet" -JUMPBOX_SUBNET_PREFIX="10.0.2.0/27" # /27 = 32 IPs -``` - -### 7.2 Crear Resource Group de Red - -```bash -az group create \ - --name $AILZ_RG \ - --location $LOCATION \ - --tags purpose=mini-ailz owner=demo -``` - -### 7.3 Crear Virtual Network con Subredes - -```bash -# Crear VNet -az network vnet create \ - --resource-group $AILZ_RG \ - --name $VNET_NAME \ - --address-prefix $VNET_PREFIX \ - --location $LOCATION \ - --tags purpose=mini-ailz - -# Subred para Private Endpoints -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name $PE_SUBNET_NAME \ - --address-prefix $PE_SUBNET_PREFIX - -# Subred para Container Apps Environment -# IMPORTANTE: Container Apps necesita /23 mínimo y la subred debe estar delegada -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name $ACA_SUBNET_NAME \ - --address-prefix $ACA_SUBNET_PREFIX - -# Subred para JumpBox -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name $JUMPBOX_SUBNET_NAME \ - --address-prefix $JUMPBOX_SUBNET_PREFIX -``` - -> **Nota sobre el tamaño de subred para Container Apps:** Container Apps Environment en modo VNet-integrated requiere una subred de al menos `/23` (512 direcciones). Esto es un requisito de la plataforma para manejar la infraestructura interna del entorno. - -### 7.4 Crear las 6 Private DNS Zones Requeridas - -Cada zona debe vincularse (link) a la VNet para que la resolución DNS funcione: - -```bash -# Lista de zonas requeridas por ContentFlow -DNS_ZONES=( - "privatelink.blob.core.windows.net" - "privatelink.documents.azure.com" - "privatelink.azconfig.io" - "privatelink.azurecr.io" - "privatelink.cognitiveservices.azure.com" - "privatelink.azurecontainerapps.io" -) - -# Crear cada zona y vincularla a la VNet -for ZONE in "${DNS_ZONES[@]}"; do - echo "Creando DNS Zone: $ZONE" - - # Crear la zona - az network private-dns zone create \ - --resource-group $AILZ_RG \ - --name "$ZONE" \ - --tags purpose=mini-ailz - - # Vincular la zona a la VNet (CRÍTICO - sin esto la resolución DNS no funciona) - LINK_NAME=$(echo "$ZONE" | sed 's/\./-/g')-link - az network private-dns link vnet create \ - --resource-group $AILZ_RG \ - --zone-name "$ZONE" \ - --name "$LINK_NAME" \ - --virtual-network "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" \ - --registration-enabled false -done - -echo "✓ 6 Private DNS Zones creadas y vinculadas a la VNet" -``` - -### 7.5 (Opcional) Crear Private DNS Zone para Key Vault - -```bash -# Solo si planeas usar Key Vault con Private Endpoint -az network private-dns zone create \ - --resource-group $AILZ_RG \ - --name "privatelink.vaultcore.azure.net" \ - --tags purpose=mini-ailz - -az network private-dns link vnet create \ - --resource-group $AILZ_RG \ - --zone-name "privatelink.vaultcore.azure.net" \ - --name "privatelink-vaultcore-azure-net-link" \ - --virtual-network "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" \ - --registration-enabled false -``` - -### 7.6 Crear JumpBox VM - -La JumpBox es necesaria para acceder a los servicios internos después del despliegue. Tienes **tres opciones**: - -#### Opción A: JumpBox con Azure Bastion (Recomendada para Demo) - -Azure Bastion provee acceso seguro RDP/SSH sin exponer IP pública en la VM. - -```bash -# Crear subred para Azure Bastion (REQUIERE nombre exacto y mínimo /26) -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name "AzureBastionSubnet" \ - --address-prefix "10.0.3.0/26" - -# Crear IP pública para Bastion -az network public-ip create \ - --resource-group $AILZ_RG \ - --name "pip-bastion-demo" \ - --sku Standard \ - --allocation-method Static \ - --location $LOCATION - -# Crear Azure Bastion (Developer SKU - más económico) -az network bastion create \ - --resource-group $AILZ_RG \ - --name "bastion-demo" \ - --public-ip-address "pip-bastion-demo" \ - --vnet-name $VNET_NAME \ - --sku Developer \ - --location $LOCATION - -# Crear la JumpBox VM (sin IP pública - Bastion proporciona acceso) -az vm create \ - --resource-group $AILZ_RG \ - --name "jmp-ailz-demo" \ - --image "MicrosoftWindowsDesktop:windows-11:win11-24h2-pro:latest" \ - --size "Standard_B2ms" \ - --vnet-name $VNET_NAME \ - --subnet $JUMPBOX_SUBNET_NAME \ - --public-ip-address "" \ - --admin-username "azureuser" \ - --admin-password "$(openssl rand -base64 16)A1!" \ - --tags role=jumpbox purpose=mini-ailz \ - --location $LOCATION -``` - -> **Importante:** Anota la contraseña generada. También puedes usar `--admin-password` con un valor específico que controles. - -#### Opción B: JumpBox con IP Pública Temporal (Más Simple) - -```bash -# JumpBox con IP pública (solo para demo, desactivar después) -az vm create \ - --resource-group $AILZ_RG \ - --name "jmp-ailz-demo" \ - --image "MicrosoftWindowsDesktop:windows-11:win11-24h2-pro:latest" \ - --size "Standard_B2ms" \ - --vnet-name $VNET_NAME \ - --subnet $JUMPBOX_SUBNET_NAME \ - --admin-username "azureuser" \ - --admin-password "TuPasswordSeguro123!" \ - --tags role=jumpbox purpose=mini-ailz \ - --nsg-rule RDP \ - --location $LOCATION -``` - -> **Seguridad:** Restringe RDP solo a tu IP: -> ```bash -> MY_IP=$(curl -s https://api.ipify.org) -> az network nsg rule update \ -> --resource-group $AILZ_RG \ -> --nsg-name "jmp-ailz-demoNSG" \ -> --name "rdp" \ -> --source-address-prefixes "$MY_IP/32" -> ``` - -#### Opción C: JumpBox Linux (Más Económica) - -```bash -az vm create \ - --resource-group $AILZ_RG \ - --name "jmp-ailz-demo" \ - --image "Ubuntu2404" \ - --size "Standard_B2s" \ - --vnet-name $VNET_NAME \ - --subnet $JUMPBOX_SUBNET_NAME \ - --admin-username "azureuser" \ - --generate-ssh-keys \ - --tags role=jumpbox purpose=mini-ailz \ - --public-ip-address "" \ - --location $LOCATION -``` - -### 7.7 (Opcional) Crear Observabilidad Compartida - -Si quieres simular el escenario enterprise donde Log Analytics y App Insights son compartidos: - -```bash -# Log Analytics Workspace compartido -az monitor log-analytics workspace create \ - --resource-group $AILZ_RG \ - --workspace-name "log-ailz-shared" \ - --sku PerGB2018 \ - --location $LOCATION \ - --tags purpose=mini-ailz - -# Application Insights compartido -LOG_WS_ID=$(az monitor log-analytics workspace show \ - --resource-group $AILZ_RG \ - --workspace-name "log-ailz-shared" \ - --query id -o tsv) - -az monitor app-insights component create \ - --resource-group $AILZ_RG \ - --app "appi-ailz-shared" \ - --location $LOCATION \ - --workspace "$LOG_WS_ID" \ - --tags purpose=mini-ailz -``` - -> **Nota:** Si no creas estos recursos, ContentFlow en modo `ailz-integrated` **creará sus propios** Log Analytics y App Insights (el código Bicep lo contempla). Solo necesitas crearlos si quieres probar el escenario de observabilidad compartida. - -### 7.8 Verificar la Mini-AILZ - -```bash -echo "=== Verificación de Mini-AILZ ===" - -echo "VNet:" -az network vnet show -g $AILZ_RG -n $VNET_NAME --query "{name:name, address:addressSpace.addressPrefixes[0]}" -o table - -echo "" -echo "Subredes:" -az network vnet subnet list -g $AILZ_RG --vnet-name $VNET_NAME --query "[].{name:name, prefix:addressPrefix}" -o table - -echo "" -echo "Private DNS Zones:" -az network private-dns zone list -g $AILZ_RG --query "[].{name:name, links:numberOfVirtualNetworkLinks}" -o table - -echo "" -echo "JumpBox VM:" -az vm list -g $AILZ_RG --query "[].{name:name, size:hardwareProfile.vmSize, tags:tags.role}" -o table -``` - -Salida esperada: -``` -=== Verificación de Mini-AILZ === -VNet: -Name Address ------------ ---------- -vnet-ailz-demo 10.0.0.0/16 - -Subredes: -Name Prefix ------------ ---------- -pe-subnet 10.0.1.0/27 -aca-env-subnet 10.0.16.0/23 -jumpbox-subnet 10.0.2.0/27 - -Private DNS Zones: -Name Links ---------------------------------------------- ----- -privatelink.blob.core.windows.net 1 -privatelink.documents.azure.com 1 -privatelink.azconfig.io 1 -privatelink.azurecr.io 1 -privatelink.cognitiveservices.azure.com 1 -privatelink.azurecontainerapps.io 1 - -JumpBox VM: -Name Size Tags ------------ ----------- -------- -jmp-ailz-demo Standard_B2ms jumpbox -``` - ---- - -## 8. Paso a Paso: Desplegar ContentFlow en Modo AILZ - -### 8.1 Obtener IDs de Recursos de la Mini-AILZ - -Usa el script incluido en ContentFlow o hazlo manualmente: - -#### Opción A: Script Automático (Recomendada) - -```bash -cd infra/scripts - -# Ejecutar con auto-set (configura variables de azd automáticamente) -./get-ailz-resources.sh --auto-set -# Cuando pregunte por el RG, ingresa: rg-mini-ailz -``` - -El script busca automáticamente: -- VNet y subredes (`pe-subnet`, `aca-env-subnet`) -- Las 6 Private DNS Zones requeridas -- JumpBox VM (busca tag `role=jumpbox` o nombre con `jmp`) -- Log Analytics y App Insights (opcionales) - -Genera un archivo `ailz-resources.env` y con `--auto-set` configura las variables en azd directamente. - -#### Opción B: Recopilación Manual - -```bash -# Obtener VNet Resource ID -VNET_ID=$(az network vnet show -g $AILZ_RG -n $VNET_NAME --query id -o tsv) -echo "VNET_ID: $VNET_ID" - -# Obtener IDs de Private DNS Zones -BLOB_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.blob.core.windows.net" --query id -o tsv) -COSMOS_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.documents.azure.com" --query id -o tsv) -APPCONFIG_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azconfig.io" --query id -o tsv) -ACR_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azurecr.io" --query id -o tsv) -COGNITIVE_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.cognitiveservices.azure.com" --query id -o tsv) -ACA_DNS_ID=$(az network private-dns zone show -g $AILZ_RG -n "privatelink.azurecontainerapps.io" --query id -o tsv) - -# Opcionales -LOG_WS_ID=$(az monitor log-analytics workspace show -g $AILZ_RG -n "log-ailz-shared" --query id -o tsv 2>/dev/null || echo "") -APPI_ID=$(az monitor app-insights component show -g $AILZ_RG --app "appi-ailz-shared" --query id -o tsv 2>/dev/null || echo "") -``` - -### 8.2 Inicializar Entorno de azd - -```bash -# Ir al raíz del repositorio -cd /ruta/a/contentflow - -# Inicializar nuevo entorno azd -azd init -e contentflow-ailz-demo - -# Configurar ubicación y suscripción -azd env set AZURE_LOCATION "$LOCATION" -azd env set AZURE_AI_FOUNDRY_LOCATION "$LOCATION" -``` - -### 8.3 Configurar Variables de Modo AILZ - -```bash -# === MODO DE DESPLIEGUE === -azd env set DEPLOYMENT_MODE "ailz-integrated" - -# === RED === -azd env set EXISTING_VNET_RESOURCE_ID "$VNET_ID" -azd env set PRIVATE_ENDPOINT_SUBNET_NAME "pe-subnet" -azd env set CONTAINER_APPS_SUBNET_NAME "aca-env-subnet" - -# === PRIVATE DNS ZONES (6 requeridas) === -azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID "$BLOB_DNS_ID" -azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID "$COSMOS_DNS_ID" -azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID "$APPCONFIG_DNS_ID" -azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID "$ACR_DNS_ID" -azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID "$COGNITIVE_DNS_ID" -azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$ACA_DNS_ID" - -# === OPCIONALES: OBSERVABILIDAD COMPARTIDA === -# Solo si creaste Log Analytics y App Insights compartidos en el paso 7.7 -if [ ! -z "$LOG_WS_ID" ]; then - azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$LOG_WS_ID" -fi -if [ ! -z "$APPI_ID" ]; then - azd env set EXISTING_APP_INSIGHTS_ID "$APPI_ID" -fi - -# === Principal ID (tu usuario para RBAC) === -PRINCIPAL_ID=$(az ad signed-in-user show --query id -o tsv) -azd env set AZURE_PRINCIPAL_ID "$PRINCIPAL_ID" -``` - -### 8.4 Verificar Configuración - -```bash -# Listar todas las variables del entorno azd -azd env get-values | grep -E "DEPLOYMENT_MODE|EXISTING_|PRIVATE_|CONTAINER_APPS_SUBNET|AZURE_LOCATION" -``` - -Salida esperada: -``` -DEPLOYMENT_MODE="ailz-integrated" -EXISTING_VNET_RESOURCE_ID="/subscriptions/.../virtualNetworks/vnet-ailz-demo" -PRIVATE_ENDPOINT_SUBNET_NAME="pe-subnet" -CONTAINER_APPS_SUBNET_NAME="aca-env-subnet" -EXISTING_BLOB_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.blob.core.windows.net" -EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.documents.azure.com" -EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azconfig.io" -EXISTING_ACR_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azurecr.io" -EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.cognitiveservices.azure.com" -EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID="/subscriptions/.../privateDnsZones/privatelink.azurecontainerapps.io" -AZURE_LOCATION="eastus2" -``` - -### 8.5 Desplegar - -```bash -# Despliegue completo (provision + build + deploy) -azd up -``` - -**¿Qué sucede durante `azd up`?** - -1. **Pre-provision hook**: Valida que Azure CLI, azd y Docker estén instalados -2. **Bicep Provisioning**: - - Valida parámetros AILZ (all 6 DNS zones + VNet + subredes) - - Crea User Assigned Identity - - Crea Storage Account con PE en `pe-subnet` → registra en `privatelink.blob.core.windows.net` - - Crea Cosmos DB con PE en `pe-subnet` → registra en `privatelink.documents.azure.com` - - Crea App Configuration con PE en `pe-subnet` → registra en `privatelink.azconfig.io` - - Crea Container Registry **Premium** con PE en `pe-subnet` → registra en `privatelink.azurecr.io` - - Crea Container Apps Environment **interno** en `aca-env-subnet` - - Crea Container Apps (API, Worker, Web) con ingress **interno** - - Crea AI Foundry Hub & Project - - Si no proporcionaste Log Analytics/App Insights existentes: crea nuevos - - Configura RBAC para la Managed Identity -3. **Container Build**: Construye imágenes Docker para API, Worker, Web -4. **Push to ACR**: Sube imágenes a Container Registry (vía PE si estás en la VNet, o vía Azure CLI si públicamente) -5. **Deploy to Container Apps**: Despliega las apps (internamente accesibles) -6. **Post-deploy hook**: Muestra endpoints (internos) - -> **⚠️ IMPORTANTE sobre el push a ACR:** En modo AILZ, el ACR tiene `publicNetworkAccess: Disabled`. Si ejecutas `azd up` desde tu máquina local (fuera de la VNet), el push de imágenes **podría fallar**. Ver sección de Troubleshooting para soluciones. - -### 8.6 Verificar Despliegue - -```bash -# Verificar que los Private Endpoints se crearon correctamente -az network private-endpoint list \ - --resource-group $(azd env get-value AZURE_RESOURCE_GROUP) \ - --query "[].{name:name, status:privateLinkServiceConnections[0].privateLinkServiceConnectionState.status}" \ - -o table -``` - -Salida esperada: -``` -Name Status ------------------------ --------- -stXXXXX-blob-pe Approved -stXXXXX-queue-pe Approved -cosmos-XXXXX-pe Approved -appcs-XXXXX-pe Approved -crXXXXX-pe Approved -``` - ---- - -## 9. Acceso y Pruebas de Conectividad Privada - -### 9.1 Acceder vía JumpBox - -Dado que todos los servicios son internos, necesitas estar dentro de la VNet: - -```bash -# Conectar a la JumpBox vía Bastion (Portal Azure) -# O vía Azure CLI: -az network bastion ssh \ - --resource-group $AILZ_RG \ - --name "bastion-demo" \ - --target-resource-id $(az vm show -g $AILZ_RG -n "jmp-ailz-demo" --query id -o tsv) \ - --auth-type password \ - --username azureuser -``` - -### 9.2 Conseguir URLs Internas - -Las URLs de Container Apps en modo interno tienen el formato: -``` -https://...azurecontainerapps.io -``` - -Pero solo son resolvibles dentro de la VNet (gracias a la Private DNS Zone `privatelink.azurecontainerapps.io`). - -```bash -# Obtener URLs internas desde azd -azd env get-values | grep "SERVICE_.*_URI" - -# O directamente: -API_URL=$(az containerapp show -g $(azd env get-value AZURE_RESOURCE_GROUP) -n $(azd env get-value API_CONTAINER_APP_NAME) --query "properties.configuration.ingress.fqdn" -o tsv) -WEB_URL=$(az containerapp show -g $(azd env get-value AZURE_RESOURCE_GROUP) -n $(azd env get-value WEB_CONTAINER_APP_NAME) --query "properties.configuration.ingress.fqdn" -o tsv) - -echo "API: https://$API_URL" -echo "Web: https://$WEB_URL" -``` - -### 9.3 Probar desde la JumpBox - -Desde la JumpBox (Windows): -```powershell -# Verificar resolución DNS (debe resolver a IP privada 10.x.x.x) -nslookup - -# Probar API health check -Invoke-WebRequest -Uri "https:///health" -UseBasicParsing - -# Abrir navegador para la Web UI -Start-Process "https://" -``` - -Desde la JumpBox (Linux): -```bash -# Verificar resolución DNS -nslookup -# Debe retornar IP privada (10.x.x.x), NO una IP pública - -# Probar API -curl -k https:///health - -# Probar que los Private Endpoints resuelven correctamente -nslookup .blob.core.windows.net -# Debe retornar privatelink.blob.core.windows.net → 10.x.x.x - -nslookup .documents.azure.com -# Debe retornar privatelink.documents.azure.com → 10.x.x.x -``` - -### 9.4 Probar Resolución DNS de Cada Servicio - -```bash -# Desde la JumpBox, verificar que todos los PEs resuelven a IPs privadas -SERVICES=( - ".blob.core.windows.net" - ".queue.core.windows.net" - ".documents.azure.com" - ".azconfig.io" - ".azurecr.io" -) - -for SVC in "${SERVICES[@]}"; do - echo "=== $SVC ===" - nslookup $SVC - echo "" -done -``` - ---- - -## 10. Mapa Completo de Parámetros - -### Variables de Entorno azd → Parámetros Bicep - -| Variable azd | Parámetro Bicep | Requerido en AILZ | Default | Descripción | -|---|---|---|---|---| -| `DEPLOYMENT_MODE` | `deploymentMode` | ✅ | — | Debe ser `ailz-integrated` | -| `AZURE_ENV_NAME` | `environmentName` | ✅ | — | Nombre del entorno | -| `AZURE_LOCATION` | `location` | ✅ | — | Región Azure | -| `AZURE_AI_FOUNDRY_LOCATION` | `foundryLocation` | ✅ | — | Región para AI Foundry | -| `AZURE_PRINCIPAL_ID` | `principalId` | ⬜ | `""` | Tu Object ID para RBAC | -| `EXISTING_VNET_RESOURCE_ID` | `existingVnetResourceId` | ✅ | `""` | Resource ID completo de la VNet | -| `PRIVATE_ENDPOINT_SUBNET_NAME` | `privateEndpointSubnetName` | ✅ | `pe-subnet` | Nombre de la subred para PEs | -| `CONTAINER_APPS_SUBNET_NAME` | `containerAppsSubnetName` | ✅ | `aca-env-subnet` | Nombre de la subred para CAE | -| `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | `existingCognitiveServicesPrivateDnsZoneId` | ✅ | `""` | DNS Zone para AI Services | -| `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | `existingBlobPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Blob Storage | -| `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | `existingCosmosPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Cosmos DB | -| `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | `existingAppConfigPrivateDnsZoneId` | ✅ | `""` | DNS Zone para App Config | -| `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | `existingAcrPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Container Registry | -| `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | `existingContainerAppsEnvPrivateDnsZoneId` | ✅ | `""` | DNS Zone para Container Apps | -| `EXISTING_KEY_VAULT_PRIVATE_DNS_ZONE_ID` | `existingKeyVaultPrivateDnsZoneId` | ⬜ | `""` | DNS Zone para Key Vault | -| `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | `existingLogAnalyticsWorkspaceId` | ⬜ | `""` | Log Analytics compartido | -| `EXISTING_APP_INSIGHTS_ID` | `existingAppInsightsId` | ⬜ | `""` | App Insights compartido | - -### Formato de Resource IDs - -``` -VNet: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{name} -DNS Zone: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Network/privateDnsZones/{zone-name} -Log Analyt: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.OperationalInsights/workspaces/{name} -App Insigh: /subscriptions/{subId}/resourceGroups/{rg}/providers/Microsoft.Insights/components/{name} -``` - ---- - -## 11. Cambios por Recurso en Modo AILZ - -### Comparativa Detallada - -| Recurso | Basic | AILZ-Integrated | -|---|---|---| -| **Storage Account** | SKU: Standard_LRS, Public | SKU: Standard_LRS, **PE (blob+queue)**, `Deny` ACL | -| **Cosmos DB** | Serverless, Public | Serverless, **PE**, `Disabled` public access | -| **App Configuration** | Standard, Public | Standard, **PE**, `Disabled` public access | -| **Container Registry** | **Standard** | **Premium** (requerido para PE), **PE** | -| **Container Apps Env** | Public, External LB | **VNet-integrated**, **Internal LB**, `aca-env-subnet` | -| **Container Apps (API)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | -| **Container Apps (Worker)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | -| **Container Apps (Web)** | `externalIngress: true` | `externalIngress: false` (solo VNet) | -| **AI Foundry** | Public | **PE** para Cognitive Services | -| **Log Analytics** | Creado nuevo (basic) | Usa existente O crea nuevo | -| **App Insights** | Creado nuevo (basic) | Usa existente O crea nuevo | -| **Managed Identity** | Sin cambios | Sin cambios | - -### Nomenclatura de Private Endpoints - -ContentFlow usa una convención consistente: - -``` -{resourceName}-{service}-pe → Nombre del Private Endpoint -{resourceName}-{service}-plsc → Private Link Service Connection -{service}-dns-zone-group → DNS Zone Group -{service}-config → DNS Zone Group Config -``` - -Ejemplos: -- `st4a7b2c-blob-pe` / `st4a7b2c-blob-plsc` -- `st4a7b2c-queue-pe` / `st4a7b2c-queue-plsc` -- `cosmos-4a7b2c-pe` / `cosmos-4a7b2c-cosmos-plsc` -- `appcs-4a7b2c-pe` / `appcs-4a7b2c-app-config-plsc` -- `cr4a7b2c-pe` / `cr4a7b2c-acr-plsc` - ---- - -## 12. Troubleshooting - -### Problema 1: Error "ACR push failed" durante `azd up` - -**Causa:** ACR tiene `publicNetworkAccess: Disabled`. Tu máquina local no puede hacer push de imágenes. - -**Soluciones:** - -**Solución A - Temporalmente habilitar acceso público al ACR:** -```bash -# Antes de azd deploy -ACR_NAME=$(azd env get-value AZURE_CONTAINER_REGISTRY_NAME) -CF_RG=$(azd env get-value AZURE_RESOURCE_GROUP) - -# Habilitar temporalmente -az acr update -n $ACR_NAME -g $CF_RG --public-network-enabled true - -# Hacer el deploy -azd deploy - -# Volver a deshabilitar -az acr update -n $ACR_NAME -g $CF_RG --public-network-enabled false -``` - -**Solución B - Build y push desde la JumpBox:** -```bash -# Ejecutar azd up desde la JumpBox (dentro de la VNet) -# Requiere instalar azd, az cli, docker en la JumpBox -``` - -**Solución C - Usar ACR Tasks (build en la nube):** -```bash -# ACR Tasks puede hacer build sin necesidad de Docker local -az acr build --registry $ACR_NAME -t contentflow-api:latest ./contentflow-api/ -az acr build --registry $ACR_NAME -t contentflow-worker:latest ./contentflow-worker/ -az acr build --registry $ACR_NAME -t contentflow-web:latest ./contentflow-web/ -``` - -### Problema 2: Error "VNet subnet not found" - -**Causa:** Los nombres de subred no coinciden con los esperados. - -**Solución:** -```bash -# Verificar nombres exactos -az network vnet subnet list -g $AILZ_RG --vnet-name $VNET_NAME --query "[].name" -o tsv - -# Si los nombres son diferentes, ajustar: -azd env set PRIVATE_ENDPOINT_SUBNET_NAME "tu-nombre-de-pe-subnet" -azd env set CONTAINER_APPS_SUBNET_NAME "tu-nombre-de-aca-subnet" -``` - -### Problema 3: Error de validación "fail('existingXXX is required')" - -**Causa:** Falta algún parámetro requerido para modo `ailz-integrated`. - -**Solución:** Ejecutar el script de verificación: -```bash -# Verificar todas las variables están configuradas -REQUIRED_VARS=( - "DEPLOYMENT_MODE" - "EXISTING_VNET_RESOURCE_ID" - "EXISTING_BLOB_PRIVATE_DNS_ZONE_ID" - "EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID" - "EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID" - "EXISTING_ACR_PRIVATE_DNS_ZONE_ID" - "EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID" - "EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID" -) - -for VAR in "${REQUIRED_VARS[@]}"; do - VALUE=$(azd env get-value $VAR 2>/dev/null) - if [ -z "$VALUE" ]; then - echo "❌ FALTA: $VAR" - else - echo "✓ $VAR configurada" - fi -done -``` - -### Problema 4: DNS no resuelve a IP privada desde JumpBox - -**Causa:** La Private DNS Zone no está vinculada a la VNet, o el DNS Zone Group no se creó. - -**Solución:** -```bash -# Verificar que cada zona tiene un VNet link -for ZONE in "privatelink.blob.core.windows.net" "privatelink.documents.azure.com" "privatelink.azconfig.io" "privatelink.azurecr.io" "privatelink.cognitiveservices.azure.com" "privatelink.azurecontainerapps.io"; do - echo "=== $ZONE ===" - az network private-dns link vnet list --zone-name $ZONE -g $AILZ_RG --query "[].{name:name, state:virtualNetworkLinkState}" -o table -done - -# Si falta un link: -az network private-dns link vnet create \ - --resource-group $AILZ_RG \ - --zone-name "privatelink.blob.core.windows.net" \ - --name "blob-vnet-link" \ - --virtual-network "$VNET_ID" \ - --registration-enabled false -``` - -### Problema 5: Container Apps no inician (health check failed) - -**Causa:** Las Container Apps no pueden alcanzar los servicios backend (Storage, Cosmos) porque los PEs no están configurados correctamente o los DNS Zone Groups no registraron los records A. - -**Solución:** -```bash -# Verificar logs de Container Apps -az containerapp logs show \ - -n $(azd env get-value API_CONTAINER_APP_NAME) \ - -g $(azd env get-value AZURE_RESOURCE_GROUP) \ - --type system - -# Verificar que los A records existen en las DNS zones -az network private-dns record-set a list -g $AILZ_RG -z "privatelink.blob.core.windows.net" -o table -az network private-dns record-set a list -g $AILZ_RG -z "privatelink.documents.azure.com" -o table -``` - -### Problema 6: Permisos insuficientes para crear PE en subnet de otro RG - -**Causa:** Tu usuario no tiene `Network Contributor` en el RG de la VNet. - -**Solución:** -```bash -# Asignar permisos en el RG de red -USER_OBJ_ID=$(az ad signed-in-user show --query id -o tsv) - -az role assignment create \ - --assignee $USER_OBJ_ID \ - --role "Network Contributor" \ - --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG" - -az role assignment create \ - --assignee $USER_OBJ_ID \ - --role "Private DNS Zone Contributor" \ - --scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG" -``` - -### Problema 7: Error "Container Apps Environment subnet too small" - -**Causa:** La subred `aca-env-subnet` es menor a `/23`. - -**Solución:** Recrear la subred con el tamaño correcto (requiere borrar la existente primero si no tiene recursos): -```bash -az network vnet subnet delete -g $AILZ_RG --vnet-name $VNET_NAME -n $ACA_SUBNET_NAME -az network vnet subnet create \ - --resource-group $AILZ_RG \ - --vnet-name $VNET_NAME \ - --name $ACA_SUBNET_NAME \ - --address-prefix "10.0.16.0/23" -``` - ---- - -## 13. Costos Estimados de la Mini-AILZ - -### Recursos de Red (RG: rg-mini-ailz) - -| Recurso | SKU/Tier | Costo Estimado (USD/mes) | -|---|---|---| -| VNet + Subredes | Gratuito | $0 | -| Private DNS Zones (6) | $0.50/zona | ~$3 | -| Azure Bastion | Developer SKU | ~$5.50 | -| JumpBox VM (B2ms, Windows) | Standard_B2ms | ~$60 (24/7) | -| JumpBox VM (B2s, Linux) | Standard_B2s | ~$15 (24/7) | - -**Tip para reducir costos de demo:** -```bash -# Apagar la JumpBox cuando no la uses -az vm deallocate -g $AILZ_RG -n "jmp-ailz-demo" - -# Encenderla cuando la necesites -az vm start -g $AILZ_RG -n "jmp-ailz-demo" -``` - -### Recursos ContentFlow Adicionales en AILZ vs Basic - -| Recurso | Cambio en AILZ | Impacto en Costo | -|---|---|---| -| Container Registry | Standard → **Premium** | +~$45/mes | -| Private Endpoints (5-6) | Cada PE tiene costo | +~$5/mes total | -| Procesamiento de PE (datos) | Por GB procesado | Mínimo en demo | - -### Costo Total Estimado de Demo (Mini-AILZ + ContentFlow) - -| Componente | Costo Estimado | -|---|---| -| Mini-AILZ (red + JumpBox Linux + Bastion) | ~$24/mes | -| ContentFlow AILZ (Premium ACR, PEs) delta vs Basic | ~$50/mes | -| ContentFlow base (Storage, Cosmos, Container Apps, AI) | ~$30-80/mes | -| **Total estimado** | **~$100-150/mes** | - -> **Nota:** Los costos de AI Foundry (GPT-4.1) son por uso (tokens). El estimado base asume uso mínimo. Apaga la JumpBox cuando no la uses para reducir costos. - ---- - -## 14. Limpieza de Recursos - -### Eliminar ContentFlow - -```bash -# Eliminar todos los recursos de ContentFlow -azd down --force --purge -``` - -> `--purge` elimina los recursos en soft-delete (Cosmos DB, Key Vault, AI Services). - -### Eliminar la Mini-AILZ - -```bash -# Eliminar todo el Resource Group de red -az group delete --name $AILZ_RG --yes --no-wait -``` - -### Limpieza Selectiva (Mantener la Red) - -Si quieres mantener la Mini-AILZ para futuras demos: -```bash -# Solo eliminar ContentFlow -azd down --force --purge - -# Apagar JumpBox para ahorrar -az vm deallocate -g $AILZ_RG -n "jmp-ailz-demo" -``` - ---- - -## Apéndice A: Script Completo de Creación de Mini-AILZ - -Para conveniencia, aquí está el script completo que crea toda la Mini-AILZ de un solo paso: - -```bash -#!/bin/bash -# create-mini-ailz.sh - Crea una AI Landing Zone mínima para demo de ContentFlow -set -e - -# === CONFIGURACIÓN (AJUSTAR SEGÚN TU AMBIENTE) === -SUBSCRIPTION_ID=$(az account show --query id -o tsv) -LOCATION="eastus2" -AILZ_RG="rg-mini-ailz" -VNET_NAME="vnet-ailz-demo" -VNET_PREFIX="10.0.0.0/16" -PE_SUBNET_PREFIX="10.0.1.0/27" -ACA_SUBNET_PREFIX="10.0.16.0/23" -JUMPBOX_SUBNET_PREFIX="10.0.2.0/27" -BASTION_SUBNET_PREFIX="10.0.3.0/26" - -echo "=== Creando Mini-AILZ para ContentFlow Demo ===" -echo "Suscripción: $SUBSCRIPTION_ID" -echo "Ubicación: $LOCATION" -echo "Resource Group: $AILZ_RG" -echo "" - -# 1. Resource Group -echo "[1/6] Creando Resource Group..." -az group create -n $AILZ_RG -l $LOCATION --tags purpose=mini-ailz -o none - -# 2. VNet + Subredes -echo "[2/6] Creando VNet y subredes..." -az network vnet create -g $AILZ_RG -n $VNET_NAME --address-prefix $VNET_PREFIX -l $LOCATION -o none -az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "pe-subnet" --address-prefix $PE_SUBNET_PREFIX -o none -az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "aca-env-subnet" --address-prefix $ACA_SUBNET_PREFIX -o none -az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "jumpbox-subnet" --address-prefix $JUMPBOX_SUBNET_PREFIX -o none -az network vnet subnet create -g $AILZ_RG --vnet-name $VNET_NAME -n "AzureBastionSubnet" --address-prefix $BASTION_SUBNET_PREFIX -o none -echo " ✓ VNet con 4 subredes creada" - -# 3. Private DNS Zones -echo "[3/6] Creando Private DNS Zones..." -VNET_ID="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$AILZ_RG/providers/Microsoft.Network/virtualNetworks/$VNET_NAME" -DNS_ZONES=( - "privatelink.blob.core.windows.net" - "privatelink.documents.azure.com" - "privatelink.azconfig.io" - "privatelink.azurecr.io" - "privatelink.cognitiveservices.azure.com" - "privatelink.azurecontainerapps.io" -) -for ZONE in "${DNS_ZONES[@]}"; do - az network private-dns zone create -g $AILZ_RG -n "$ZONE" -o none - LINK_NAME=$(echo "$ZONE" | sed 's/\./-/g')-link - az network private-dns link vnet create -g $AILZ_RG -z "$ZONE" -n "$LINK_NAME" -v "$VNET_ID" -e false -o none -done -echo " ✓ 6 Private DNS Zones creadas y vinculadas" - -# 4. Azure Bastion -echo "[4/6] Creando Azure Bastion..." -az network public-ip create -g $AILZ_RG -n "pip-bastion-demo" --sku Standard --allocation-method Static -l $LOCATION -o none -az network bastion create -g $AILZ_RG -n "bastion-demo" --public-ip-address "pip-bastion-demo" --vnet-name $VNET_NAME --sku Developer -l $LOCATION -o none -echo " ✓ Azure Bastion (Developer SKU) creado" - -# 5. JumpBox VM (Linux para costo mínimo) -echo "[5/6] Creando JumpBox VM..." -az vm create -g $AILZ_RG -n "jmp-ailz-demo" \ - --image "Ubuntu2404" --size "Standard_B2s" \ - --vnet-name $VNET_NAME --subnet "jumpbox-subnet" \ - --admin-username "azureuser" --generate-ssh-keys \ - --tags role=jumpbox purpose=mini-ailz \ - --public-ip-address "" -l $LOCATION -o none -echo " ✓ JumpBox Linux creada" - -# 6. Resumen -echo "" -echo "[6/6] Verificación..." -echo "" -echo "=== Mini-AILZ Creada Exitosamente ===" -echo "" -echo "Resource Group: $AILZ_RG" -echo "VNet: $VNET_NAME ($VNET_PREFIX)" -echo "Subredes:" -echo " - pe-subnet: $PE_SUBNET_PREFIX" -echo " - aca-env-subnet: $ACA_SUBNET_PREFIX" -echo " - jumpbox-subnet: $JUMPBOX_SUBNET_PREFIX" -echo " - AzureBastionSubnet: $BASTION_SUBNET_PREFIX" -echo "" -echo "Private DNS Zones: 6 zonas creadas y vinculadas" -echo "JumpBox: jmp-ailz-demo (acceso vía Bastion)" -echo "" -echo "=== Siguiente paso ===" -echo "Ejecuta el script de descubrimiento de ContentFlow:" -echo " cd infra/scripts && ./get-ailz-resources.sh --auto-set" -echo "Luego despliega:" -echo " azd env set DEPLOYMENT_MODE ailz-integrated" -echo " azd up" -``` - ---- - -## Apéndice B: Diagrama de Flujo de Despliegue - -``` -┌────────────────────────┐ -│ 1. Preparar Mini-AILZ │ -│ (create-mini-ailz.sh) │ -└────────┬───────────────┘ - │ - ▼ -┌────────────────────────┐ -│ 2. Obtener IDs │ -│ (get-ailz-resources │ -│ --auto-set) │ -└────────┬───────────────┘ - │ - ▼ -┌────────────────────────┐ -│ 3. Configurar azd │ -│ DEPLOYMENT_MODE= │ -│ ailz-integrated │ -└────────┬───────────────┘ - │ - ▼ -┌────────────────────────┐ -│ 4. azd up │ -│ (provision + deploy) │ -└────────┬───────────────┘ - │ - ▼ -┌────────────────────────┐ ┌──────────────────────┐ -│ 5. Conectar a │────▶│ 6. Probar servicios │ -│ JumpBox vía Bastion │ │ internos (API, Web) │ -└────────────────────────┘ └──────────────────────┘ -``` - ---- - -> **Última actualización:** Febrero 2026 -> **Aplica a:** ContentFlow con plantillas Bicep para despliegue `basic` y `ailz-integrated` diff --git a/Analysis/05-mini-ailz.md b/Analysis/05-mini-ailz.md deleted file mode 100644 index f7e6714..0000000 --- a/Analysis/05-mini-ailz.md +++ /dev/null @@ -1,1002 +0,0 @@ -# Mini-AILZ con Terraform AVM Pattern Module para ContentFlow - -> **Documento técnico operativo** — Despliegue de una AI Landing Zone mínima usando el módulo oficial Terraform AVM `Azure/avm-ptn-aiml-landing-zone`, optimizada para el costo mínimo necesario para ContentFlow en modo `ailz-integrated`. - ---- - -## Tabla de Contenidos - -1. [Objetivo y Contexto](#1-objetivo-y-contexto) -2. [¿Por qué el Pattern Module en lugar de Azure CLI Manual?](#2-por-qué-el-pattern-module-en-lugar-de-azure-cli-manual) -3. [Análisis del Módulo: Todos los Recursos vs Lo Mínimo](#3-análisis-del-módulo-todos-los-recursos-vs-lo-mínimo) -4. [Mapeo: Requisitos ContentFlow → Parámetros del Módulo](#4-mapeo-requisitos-contentflow--parámetros-del-módulo) -5. [Configuración Terraform Mínima Completa](#5-configuración-terraform-mínima-completa) -6. [Conexión de Outputs con ContentFlow azd](#6-conexión-de-outputs-con-contentflow-azd) -7. [Fix Conocido: storage_use_azuread](#7-fix-conocido-storage_use_azuread) -8. [Paso a Paso: Despliegue](#8-paso-a-paso-despliegue) -9. [Comparación de Costo: Full vs Mini](#9-comparación-de-costo-full-vs-mini) -10. [Diagrama de Arquitectura Resultante](#10-diagrama-de-arquitectura-resultante) -11. [Notas de Operación y Troubleshooting](#11-notas-de-operación-y-troubleshooting) -12. [Limpieza de Recursos](#12-limpieza-de-recursos) - ---- - -## 1. Objetivo y Contexto - -### Situación - -ContentFlow soporta despliegue en modo `ailz-integrated`, el cual requiere infraestructura de red pre-existente (VNet, subredes, Private DNS Zones, Container Apps Environment). En el [documento 04](04-ai_lz_options.md) se describe cómo crear esta infraestructura con comandos Azure CLI manuales. - -### Nuevo Enfoque - -En lugar de scripts manuales, usaremos el **módulo oficial Terraform AVM Pattern** para AI/ML Landing Zones: - -- **Módulo**: [`Azure/avm-ptn-aiml-landing-zone/azurerm`](https://github.com/Azure/terraform-azurerm-avm-ptn-aiml-landing-zone) -- **Versión**: `0.4.0` -- **Registry**: [Terraform Registry](https://registry.terraform.io/modules/Azure/avm-ptn-aiml-landing-zone/azurerm/latest) - -### Principio Clave: Solo lo Mínimo Indispensable - -El módulo completo despliega **más de 25 recursos** incluyendo AI Foundry, App Gateway, Firewall, APIM, Build VM, AI Search, Bing Grounding, etc. ContentFlow **no necesita la mayoría de estos**. Este documento configura **exclusivamente** lo que ContentFlow requiere para funcionar en modo privado. - ---- - -## 2. ¿Por qué el Pattern Module en lugar de Azure CLI Manual? - -| Aspecto | Azure CLI Manual (Doc 04) | Terraform AVM Module (Este Doc) | -|---|---|---| -| **Reproducibilidad** | Scripts bash con variables | Declarativo, idempotente, plan/apply | -| **Estado** | No hay tracking de estado | `terraform.tfstate` con full tracking | -| **Drift detection** | Manual (`az resource show`) | `terraform plan` detecta drift | -| **Limpieza** | `az group delete` (todo o nada) | `terraform destroy` selectivo | -| **Validación** | Post-ejecución manual | `terraform validate` + `plan` pre-apply | -| **Subredes y DNS** | Creación manual una por una | El módulo crea subredes automáticamente | -| **Private DNS Zones** | 6 comandos individuales + links | El módulo las crea y vincula | -| **Container Apps Env** | No incluido en doc 04 | Incluido con VNet integration | -| **Bastion** | Configuración manual compleja | Un flag `deploy = true` | -| **Versionamiento** | Copiar/pegar scripts | Module version pinning | -| **Soporte oficial** | Ninguno | Módulo oficial de Microsoft (AVM) | - -**Ventaja principal**: El módulo maneja internamente la creación de subredes con los nombres, tamaños y delegaciones correctas. Esto elimina errores comunes como subredes con prefijos incorrectos o sin delegación para Container Apps. - ---- - -## 3. Análisis del Módulo: Todos los Recursos vs Lo Mínimo - -### Recursos que el módulo PUEDE desplegar (despliegue completo) - -| Recurso | Default `deploy` | ¿ContentFlow lo necesita? | Acción Mini-AILZ | -|---|---|---|---| -| **VNet + Subredes** | Siempre (required) | **SÍ** | ✅ Mantener | -| **Private DNS Zones** | Siempre | **SÍ** | ✅ Mantener | -| **Log Analytics Workspace** | `true` | **SÍ** (compartido) | ✅ Mantener | -| **NSGs** | Siempre | **SÍ** (seguridad) | ✅ Mantener | -| **Container Apps Environment** | `true` | **SÍ** | ✅ Mantener | -| **Bastion** | `true` | **SÍ** (acceso a red privada) | ✅ Mantener | -| **GenAI Container Registry** | `true` | **Parcial** (ContentFlow crea el suyo) | ⚠️ Mantener* | -| **GenAI Key Vault** | Siempre | **Parcial** | ⚠️ Mantener* | -| **GenAI Storage Account** | `true` | **No directamente** | ⚠️ Mantener* | -| **GenAI App Configuration** | `true` | **No directamente** | ⚠️ Mantener* | -| **GenAI Cosmos DB** | `true` | **No directamente** | ⚠️ Mantener* | -| **AI Foundry Hub + BYOR** | `{}` (vacío = no crea) | **No** | ❌ Omitir | -| **AI Model Deployments** | `{}` (vacío = no crea) | **No** | ❌ Omitir | -| **AI Projects** | `{}` (vacío = no crea) | **No** | ❌ Omitir | -| **App Gateway** | `null` (no crea) | **No** | ❌ Omitir | -| **Azure Firewall** | `true` | **No** (demo) | ❌ Deshabilitar | -| **Build VM / Jump VM** | `true` | **No** (usamos Bastion) | ❌ Deshabilitar | -| **AI Search (BYOR)** | `{}` (vacío = no crea) | **No** | ❌ Omitir | -| **AI Search (KS)** | `true` | **No** | ❌ Deshabilitar | -| **Bing Grounding** | `true` | **No** | ❌ Deshabilitar | -| **APIM** | `true` | **No** | ❌ Deshabilitar | -| **WAF Policy** | Solo con App GW | **No** | ❌ Omitir | - -> **\*** Los recursos GenAI (Container Registry, Key Vault, Storage, App Config, Cosmos) se crean por defecto como parte de la plataforma GenAI del módulo. ContentFlow crea sus propios recursos de datos (Cosmos, Storage, App Config) durante `azd up`, pero el módulo necesita el Container Registry y Key Vault internamente. Para minimizar costo, podemos aceptar estos con configuración por defecto ligera. - -### ¿Qué NO se puede deshabilitar? - -El módulo **siempre crea** estos recursos independientemente de la configuración: - -1. **Resource Group** (si no existe) -2. **VNet** (required input) -3. **Subredes** (calculadas internamente según recursos habilitados) -4. **Private DNS Zones** (necesarias para private endpoints) -5. **NSGs** (asociados a subredes) -6. **Key Vault** (usado internamente por el módulo para secretos de GenAI) -7. **Role Assignments** (deployment user → Key Vault Admin) - ---- - -## 4. Mapeo: Requisitos ContentFlow → Parámetros del Módulo - -### Lo que ContentFlow Necesita del AILZ - -| Requisito ContentFlow | Parámetro del Módulo | Variable de Entorno `azd` | -|---|---|---| -| VNet Resource ID | `module.ailz.virtual_network.id` | `EXISTING_VNET_RESOURCE_ID` | -| Subred `pe-subnet` | Creada automáticamente por el módulo | `PRIVATE_ENDPOINT_SUBNET_NAME` | -| Subred `aca-env-subnet` | Creada automáticamente por el módulo | `CONTAINER_APPS_SUBNET_NAME` | -| DNS: `privatelink.blob.core.windows.net` | Parte de `private_dns_zones` del módulo | `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.documents.azure.com` | Parte de `private_dns_zones` del módulo | `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.azconfig.io` | Parte de `private_dns_zones` del módulo | `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.azurecr.io` | Parte de `private_dns_zones` del módulo | `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.cognitiveservices.azure.com` | Parte de `private_dns_zones` del módulo | `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | -| DNS: `privatelink.azurecontainerapps.io` | Parte de `private_dns_zones` del módulo | `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | -| Log Analytics Workspace | `module.ailz.log_analytics_workspace_id` | `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | -| Container Apps Environment | Creado por el módulo (VNet-integrated, interno) | El CAE se crea aquí, ContentFlow lo usa | - -### Nota Importante sobre Subredes - -El módulo AVM Pattern calcula y crea las subredes internamente basándose en qué recursos están habilitados. Los nombres de subredes son determinados por el módulo. ContentFlow espera subredes llamadas `pe-subnet` y `aca-env-subnet`. Puedes usar el bloque `vnet_definition.subnets` para configurar override de nombres si es necesario. - -### Nota sobre Container Apps Environment - -El módulo crea un Container Apps Environment integrado con la VNet. Esto es **exactamente lo que ContentFlow necesita**. Sin embargo, ContentFlow normalmente crea su propio CAE durante `azd up`. Para el modo Mini-AILZ con Terraform, tienes dos opciones: - -1. **Usar el CAE del módulo** — Más eficiente; ContentFlow debe configurarse para no crear un CAE propio -2. **Deshabilitar el CAE del módulo** — ContentFlow crea el suyo durante deploy, usando la subred apropiada - -La **opción recomendada es la 1** (usar el CAE del módulo) ya que garantiza la configuración correcta de VNet integration. - ---- - -## 5. Configuración Terraform Mínima Completa - -### Estructura de Archivos - -``` -mini-ailz-contentflow/ -├── terraform.tf # Providers y versiones -├── variables.tf # Variables de entrada -├── main.tf # Data sources y helpers -├── ailz.tf # Módulo AILZ con configuración mínima -├── outputs.tf # Outputs para ContentFlow -└── terraform.tfvars # Valores (opcional, gitignored) -``` - -### `terraform.tf` — Providers - -```hcl -terraform { - required_version = ">= 1.9, < 2.0" - - required_providers { - azurerm = { - source = "hashicorp/azurerm" - version = ">= 3.116, < 5.0" - } - azapi = { - source = "Azure/azapi" - version = "~> 2.0" - } - random = { - source = "hashicorp/random" - version = "~> 3.5" - } - } -} - -provider "azurerm" { - features { - cognitive_account { - purge_soft_delete_on_destroy = true - } - key_vault { - purge_soft_delete_on_destroy = true - } - resource_group { - prevent_deletion_if_contains_resources = false - } - } - - # CRÍTICO: Sin esto, las Storage Accounts dan error 403 - # Ver sección 7 para detalles - storage_use_azuread = true -} - -provider "azapi" {} -``` - -### `variables.tf` — Variables de Entrada - -```hcl -variable "location" { - type = string - description = "Región de Azure para los recursos" - default = "swedencentral" -} - -variable "resource_group_name" { - type = string - description = "Nombre del Resource Group para la Mini-AILZ" - default = "rg-mini-ailz-contentflow" -} - -variable "name_prefix" { - type = string - description = "Prefijo para nombres de recursos (máx 10 chars, minúsculas alfanuméricas)" - default = "cfailz" - - validation { - condition = length(var.name_prefix) <= 10 && can(regex("^[a-z0-9]+$", var.name_prefix)) - error_message = "name_prefix debe tener máximo 10 caracteres alfanuméricos en minúsculas." - } -} - -variable "enable_telemetry" { - type = bool - description = "Habilitar telemetría del módulo AVM" - default = false -} -``` - -### `main.tf` — Data Sources - -```hcl -data "azurerm_client_config" "current" {} -``` - -### `ailz.tf` — Módulo AILZ Configuración Mínima - -```hcl -# ============================================================================= -# Mini-AILZ para ContentFlow -# ============================================================================= -# Este archivo configura SOLO lo mínimo indispensable del módulo AVM Pattern -# para satisfacer los requisitos de ContentFlow en modo ailz-integrated: -# 1. VNet con subredes (pe-subnet, aca-env-subnet) -# 2. Private DNS Zones vinculadas a la VNet -# 3. Container Apps Environment (VNet-integrated, internal LB) -# 4. Bastion (acceso a la red privada) -# 5. Log Analytics Workspace (monitoreo compartido) -# 6. NSGs (seguridad de subredes) -# 7. Key Vault + Container Registry (requeridos internamente por el módulo) -# -# TODO lo demás está DESHABILITADO para minimizar costo: -# ✗ AI Foundry, Model Deployments, AI Projects -# ✗ App Gateway, WAF Policy -# ✗ Azure Firewall -# ✗ Build VM, Jump VM -# ✗ AI Search (BYOR y KS) -# ✗ Bing Grounding -# ✗ APIM -# ============================================================================= - -module "ailz" { - source = "Azure/avm-ptn-aiml-landing-zone/azurerm" - version = "0.4.0" - - # ── Ubicación y Resource Group ────────────────────────────────────────────── - location = var.location - resource_group_name = var.resource_group_name - - # ── VNet (REQUIRED) ──────────────────────────────────────────────────────── - # ContentFlow necesita: - # - pe-subnet: para Private Endpoints (~32 IPs, /27+) - # - aca-env-subnet: para Container Apps Environment (~512 IPs, /23) - # - # NOTA: El módulo calcula y crea las subredes internamente. - # Usamos 10.0.0.0/16 para tener espacio suficiente. - vnet_definition = { - address_space = ["10.0.0.0/16"] - enable_diagnostic_settings = false - } - - # ── Container Apps Environment ───────────────────────────────────────────── - # ContentFlow despliega API, Worker y Web como Container Apps. - # Este CAE se integra con la VNet en modo interno (solo accesible desde VNet). - container_app_environment_definition = { - internal_load_balancer_enabled = true - zone_redundancy_enabled = false # false = ahorro de costo para demo - enable_diagnostic_settings = false - workload_profile = [ - { - name = "Consumption" - workload_profile_type = "Consumption" - } - ] - } - - # ── Bastion ──────────────────────────────────────────────────────────────── - # Necesario para acceder a la red privada (UI de ContentFlow, debug, etc.) - # SKU "Basic" es más barato que "Standard" (~$140/mes vs ~$350/mes) - bastion_definition = { - deploy = true - sku = "Basic" - zones = [] # Sin zone redundancy para demo - } - - # ── Log Analytics Workspace ──────────────────────────────────────────────── - # Compartido con ContentFlow para monitoreo centralizado. - law_definition = { - deploy = true - } - - # ── AI Foundry ───────────────────────────────────────────────────────────── - # ContentFlow NO necesita AI Foundry del AILZ. - # (ContentFlow crea su propio AI Foundry durante azd up) - # Dejamos el default vacío {} = no crea hub, projects, ni BYOR resources. - ai_foundry_definition = { - create_byor = false # No crear BYOR resources (AI Search, Cosmos, KV, Storage) - ai_foundry = { - enable_diagnostic_settings = false - } - } - - # ── DESHABILITADOS ───────────────────────────────────────────────────────── - - # App Gateway: ContentFlow no usa App Gateway (acceso via Bastion/VPN) - # Default es null = no se despliega - app_gateway_definition = null - - # Azure Firewall: No necesario para demo (ahorra ~$250/mes) - firewall_definition = { - deploy = false - } - - # Build VM: No necesaria (usamos Bastion + local development) - buildvm_definition = { - deploy = false - } - - # Jump VM: No necesaria si tenemos Bastion - # (jumpvm_definition no tiene deploy flag, se controla por buildvm) - - # KS AI Search: ContentFlow no necesita AI Search federado - ks_ai_search_definition = { - deploy = false - } - - # Bing Grounding: ContentFlow no usa Bing - ks_bing_grounding_definition = { - deploy = false - } - - # APIM: ContentFlow no usa API Management - apim_definition = { - deploy = false - publisher_email = "noreply@example.com" - publisher_name = "N/A" - } - - # ── GenAI Platform Resources ─────────────────────────────────────────────── - # Estos recursos se crean por defecto como parte de la plataforma GenAI. - # ContentFlow crea sus propios Cosmos DB, Storage, App Config durante azd up, - # pero el módulo internamente necesita algunos de estos. - # Configuración mínima para reducir costo: - - genai_container_registry_definition = { - zone_redundancy_enabled = false # Ahorro: sin ZRS - enable_diagnostic_settings = false - } - - genai_key_vault_definition = { - # Acceso público habilitado para facilitar deploy desde local - # En producción: false + Private Endpoint - public_network_access_enabled = true - network_acls = { - bypass = "AzureServices" - default_action = "Deny" - } - } - - genai_storage_account_definition = { - account_replication_type = "LRS" # LRS es más barato que GRS (default) - enable_diagnostic_settings = false - } - - genai_app_configuration_definition = { - purge_protection_enabled = false # Facilita destroy para demo - enable_diagnostic_settings = false - } - - genai_cosmosdb_definition = { - analytical_storage_enabled = false # No necesario para demo - automatic_failover_enabled = false # No necesario para single-region demo - enable_diagnostic_settings = false - } - - # ── Flags y otros ────────────────────────────────────────────────────────── - # flag_platform_landing_zone = false → No crea route tables con Firewall - # (Si es true, se asume que hay un Firewall y crea UDRs para enrutar tráfico) - flag_platform_landing_zone = false - - enable_telemetry = var.enable_telemetry - name_prefix = var.name_prefix -} -``` - -### `outputs.tf` — Valores para ContentFlow - -```hcl -# ============================================================================= -# Outputs para configurar ContentFlow azd environment -# ============================================================================= -# Estos valores se usan como variables de entorno para: -# azd env set -# antes de ejecutar: azd up --deployment-mode ailz-integrated -# ============================================================================= - -output "vnet_resource_id" { - description = "VNet Resource ID → EXISTING_VNET_RESOURCE_ID" - value = module.ailz.virtual_network.id -} - -output "log_analytics_workspace_id" { - description = "Log Analytics Workspace ID → EXISTING_LOG_ANALYTICS_WORKSPACE_ID" - value = module.ailz.log_analytics_workspace_id -} - -output "subnets" { - description = "Mapa de subredes creadas (para verificar nombres)" - value = module.ailz.subnets -} - -# ============================================================================= -# NOTA SOBRE PRIVATE DNS ZONES: -# ============================================================================= -# El módulo crea las Private DNS Zones automáticamente, pero actualmente (v0.4.0) -# NO expone sus Resource IDs como outputs directos. -# -# Para obtener los IDs de las DNS Zones, después de `terraform apply`: -# -# az network private-dns zone list \ -# --resource-group \ -# --query "[].{name:name, id:id}" -o table -# -# Alternativamente, puedes usar data sources de Terraform para leerlos: -# ============================================================================= - -# Instrucciones que se imprimen después del apply -output "contentflow_setup_instructions" { - description = "Instrucciones para configurar ContentFlow con esta Mini-AILZ" - value = <<-EOT - - ╔══════════════════════════════════════════════════════════════════╗ - ║ Mini-AILZ desplegada exitosamente para ContentFlow ║ - ╚══════════════════════════════════════════════════════════════════╝ - - Próximos pasos: - - 1. Obtener los Resource IDs de las Private DNS Zones: - - az network private-dns zone list \ - --resource-group ${var.resource_group_name} \ - --query "[].{name:name, id:id}" -o table - - 2. Configurar el environment de ContentFlow: - - cd - azd env set DEPLOYMENT_MODE ailz-integrated - azd env set EXISTING_VNET_RESOURCE_ID ${module.ailz.virtual_network.id} - azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID ${module.ailz.log_analytics_workspace_id} - - # Copiar los IDs de DNS Zones del paso 1: - azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID - azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID - - 3. Desplegar ContentFlow: - - azd up - - EOT -} -``` - ---- - -## 6. Conexión de Outputs con ContentFlow azd - -Después de ejecutar `terraform apply`, necesitas pasar los valores de infraestructura a ContentFlow. - -### Script Automatizado: `configure-contentflow.sh` - -```bash -#!/bin/bash -# ============================================================================= -# configure-contentflow.sh -# Configura las variables de entorno de ContentFlow azd con los outputs -# de la Mini-AILZ desplegada con Terraform. -# ============================================================================= -set -euo pipefail - -# Verificar que estamos en el directorio correcto de Terraform -if [[ ! -f "ailz.tf" ]]; then - echo "ERROR: Ejecuta este script desde el directorio de Terraform Mini-AILZ" - exit 1 -fi - -# Directorio de ContentFlow (ajustar según tu setup) -CONTENTFLOW_DIR="${1:-../contentflow-test-001}" - -echo "Obteniendo outputs de Terraform..." -VNET_ID=$(terraform output -raw vnet_resource_id) -LAW_ID=$(terraform output -raw log_analytics_workspace_id) -RG_NAME=$(terraform output -raw 2>/dev/null | grep -oP 'resource-group \K[^ \\]+' || true) - -# Obtener el nombre real del resource group del state -RG_NAME=$(terraform show -json | python3 -c " -import sys, json -state = json.load(sys.stdin) -for r in state.get('values', {}).get('root_module', {}).get('child_modules', []): - for res in r.get('resources', []): - if res['type'] == 'azurerm_resource_group': - print(res['values']['name']) - break -" 2>/dev/null || echo "rg-mini-ailz-contentflow") - -echo "Resource Group: $RG_NAME" -echo "VNet ID: $VNET_ID" -echo "Log Analytics ID: $LAW_ID" - -echo "" -echo "Obteniendo Private DNS Zone IDs..." - -# Función helper para obtener DNS Zone ID -get_dns_zone_id() { - local zone_name=$1 - az network private-dns zone show \ - --resource-group "$RG_NAME" \ - --name "$zone_name" \ - --query id -o tsv 2>/dev/null || echo "" -} - -BLOB_DNS=$(get_dns_zone_id "privatelink.blob.core.windows.net") -COSMOS_DNS=$(get_dns_zone_id "privatelink.documents.azure.com") -APPCONFIG_DNS=$(get_dns_zone_id "privatelink.azconfig.io") -ACR_DNS=$(get_dns_zone_id "privatelink.azurecr.io") -COGNITIVE_DNS=$(get_dns_zone_id "privatelink.cognitiveservices.azure.com") -CONTAINERAPP_DNS=$(get_dns_zone_id "privatelink.azurecontainerapps.io") - -# Verificar que encontramos todas las zonas -MISSING=0 -for ZONE_VAR in BLOB_DNS COSMOS_DNS APPCONFIG_DNS ACR_DNS COGNITIVE_DNS CONTAINERAPP_DNS; do - if [[ -z "${!ZONE_VAR}" ]]; then - echo "⚠ WARNING: No se encontró DNS Zone para $ZONE_VAR" - MISSING=1 - fi -done - -if [[ $MISSING -eq 1 ]]; then - echo "" - echo "Algunas DNS Zones no se encontraron. Verifica con:" - echo " az network private-dns zone list --resource-group $RG_NAME -o table" - echo "" - echo "El módulo puede usar nombres diferentes. Ajusta manualmente si es necesario." -fi - -echo "" -echo "Configurando azd environment en: $CONTENTFLOW_DIR" - -pushd "$CONTENTFLOW_DIR" > /dev/null - -azd env set DEPLOYMENT_MODE ailz-integrated -azd env set EXISTING_VNET_RESOURCE_ID "$VNET_ID" -azd env set EXISTING_LOG_ANALYTICS_WORKSPACE_ID "$LAW_ID" -[[ -n "$BLOB_DNS" ]] && azd env set EXISTING_BLOB_PRIVATE_DNS_ZONE_ID "$BLOB_DNS" -[[ -n "$COSMOS_DNS" ]] && azd env set EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID "$COSMOS_DNS" -[[ -n "$APPCONFIG_DNS" ]] && azd env set EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID "$APPCONFIG_DNS" -[[ -n "$ACR_DNS" ]] && azd env set EXISTING_ACR_PRIVATE_DNS_ZONE_ID "$ACR_DNS" -[[ -n "$COGNITIVE_DNS" ]] && azd env set EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID "$COGNITIVE_DNS" -[[ -n "$CONTAINERAPP_DNS" ]] && azd env set EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID "$CONTAINERAPP_DNS" - -popd > /dev/null - -echo "" -echo "✅ Configuración completada. Ejecuta:" -echo " cd $CONTENTFLOW_DIR && azd up" -``` - -### Mapeo Completo de Outputs → Variables azd - -| Output Terraform / Comando az | Variable azd ContentFlow | Descripción | -|---|---|---| -| `module.ailz.virtual_network.id` | `EXISTING_VNET_RESOURCE_ID` | Resource ID de la VNet | -| `module.ailz.log_analytics_workspace_id` | `EXISTING_LOG_ANALYTICS_WORKSPACE_ID` | LAW compartido | -| `az ... privatelink.blob.core.windows.net` | `EXISTING_BLOB_PRIVATE_DNS_ZONE_ID` | DNS Zone para Blob | -| `az ... privatelink.documents.azure.com` | `EXISTING_COSMOS_PRIVATE_DNS_ZONE_ID` | DNS Zone para Cosmos | -| `az ... privatelink.azconfig.io` | `EXISTING_APP_CONFIG_PRIVATE_DNS_ZONE_ID` | DNS Zone para App Config | -| `az ... privatelink.azurecr.io` | `EXISTING_ACR_PRIVATE_DNS_ZONE_ID` | DNS Zone para ACR | -| `az ... privatelink.cognitiveservices.azure.com` | `EXISTING_COGNITIVE_SERVICES_PRIVATE_DNS_ZONE_ID` | DNS Zone para AI Services | -| `az ... privatelink.azurecontainerapps.io` | `EXISTING_CONTAINER_APPS_ENV_PRIVATE_DNS_ZONE_ID` | DNS Zone para Container Apps | -| (contenido en `pe-subnet`) | `PRIVATE_ENDPOINT_SUBNET_NAME` | Default: `pe-subnet` | -| (contenido en `aca-env-subnet`) | `CONTAINER_APPS_SUBNET_NAME` | Default: `aca-env-subnet` | - ---- - -## 7. Fix Conocido: storage_use_azuread - -### El Problema - -El módulo crea Storage Accounts con `shared_access_key_enabled = false` (default seguro). Sin embargo, el provider `azurerm` por defecto usa **autenticación por key** para operaciones de Storage. Esto produce: - -``` -Error: StatusCode=403 -- Original Error: autorest/azure: Service returned an error. -Status=403 Code="AuthorizationFailure" -``` - -### La Solución - -Agregar `storage_use_azuread = true` en el bloque del provider: - -```hcl -provider "azurerm" { - features { ... } - storage_use_azuread = true # ← CRÍTICO -} -``` - -### Prerrequisito - -El usuario que ejecuta `terraform apply` debe tener el rol **Storage Blob Data Contributor** en la suscripción o en los Resource Groups correspondientes: - -```bash -# Asignar rol a tu usuario (una vez) -az role assignment create \ - --assignee "$(az ad signed-in-user show --query id -o tsv)" \ - --role "Storage Blob Data Contributor" \ - --scope "/subscriptions/$(az account show --query id -o tsv)" -``` - ---- - -## 8. Paso a Paso: Despliegue - -### 8.1 Preparar el Entorno - -```bash -# Crear directorio de trabajo -mkdir mini-ailz-contentflow && cd mini-ailz-contentflow - -# Crear los archivos Terraform (copiar de la sección 5) -# terraform.tf, variables.tf, main.tf, ailz.tf, outputs.tf -``` - -### 8.2 Inicializar Terraform - -```bash -terraform init -``` - -Esto descarga el módulo AVM y todos sus sub-módulos (~20 módulos). - -### 8.3 Validar y Planificar - -```bash -# Validar sintaxis -terraform validate - -# Ver plan de ejecución -terraform plan -out=tfplan -``` - -El plan debería mostrar aproximadamente **15-20 recursos** a crear (vs ~45+ con el módulo completo). - -### 8.4 Aplicar - -```bash -terraform apply tfplan -``` - -Tiempo estimado: **15-25 minutos** (Bastion y Container Apps Environment son los más lentos). - -### 8.5 Verificar Recursos Creados - -```bash -# Listar recursos en el RG -az resource list \ - --resource-group rg-mini-ailz-contentflow \ - --query "[].{name:name, type:type}" \ - -o table -``` - -Deberías ver: -- Virtual Network -- Subredes (varias, creadas por el módulo) -- NSGs -- Private DNS Zones (6+) -- Bastion Host + Public IP -- Container Apps Environment -- Log Analytics Workspace -- Key Vault -- Container Registry -- Storage Account -- App Configuration -- Cosmos DB Account - -### 8.6 Configurar ContentFlow - -```bash -# Opción A: Script automatizado -chmod +x configure-contentflow.sh -./configure-contentflow.sh /path/to/contentflow - -# Opción B: Manual -cd /path/to/contentflow -azd env set DEPLOYMENT_MODE ailz-integrated -azd env set EXISTING_VNET_RESOURCE_ID "$(terraform -chdir=/path/to/mini-ailz output -raw vnet_resource_id)" -# ... (ver sección 6 para el mapeo completo) -``` - -### 8.7 Desplegar ContentFlow - -```bash -cd /path/to/contentflow -azd up -``` - ---- - -## 9. Comparación de Costo: Full vs Mini - -### Despliegue Completo del Módulo (tu test anterior en standalone-test001) - -| Recurso | SKU/Tier | Costo Estimado/mes | -|---|---|---| -| AI Foundry Hub + BYOR | S0 | ~$0 (pago por uso) | -| GPT-4.1 Model | GlobalStandard x1 | ~$0-30 (por tokens) | -| AI Search | Standard, 2 replicas | **~$500** | -| App Gateway (WAF_v2) | 2 scale units | **~$350** | -| Azure Bastion | Standard, 3 AZs | **~$350** | -| Azure Firewall | Standard, 3 AZs | **~$250** | -| Build VM | Standard_B2s | **~$30** | -| APIM | Premium x3 | **~$2,100** | -| Container Apps Env | Consumption (ZR) | ~$0-20 | -| Cosmos DB (BYOR) | Serverless | ~$0-10 | -| Cosmos DB (GenAI) | Serverless | ~$0-10 | -| Key Vault x2 | Standard | ~$0-5 | -| Storage Account x2 | ZRS/GRS | ~$5-15 | -| Container Registry | Premium (ZR) | **~$60** | -| App Configuration | Standard | ~$36 | -| Log Analytics | Per-GB | ~$0-20 | -| Bing Grounding | G1 | ~$5 | -| KS AI Search | Standard | **~$250** | -| VNet + Subredes | - | ~$0 | -| Private DNS Zones | - | ~$1-5 | -| **TOTAL ESTIMADO** | | **~$3,500-4,000/mes** | - -### Mini-AILZ (este documento) - -| Recurso | SKU/Tier | Costo Estimado/mes | -|---|---|---| -| Azure Bastion | **Basic**, sin AZs | **~$140** | -| Container Apps Env | Consumption, sin ZR | ~$0-20 | -| Container Registry | Premium (requerido para PE) | **~$50** | -| Cosmos DB (GenAI) | Serverless | ~$0-10 | -| Key Vault | Standard | ~$0-5 | -| Storage Account | **LRS** | ~$2-5 | -| App Configuration | Standard | ~$36 | -| Log Analytics | Per-GB | ~$0-20 | -| VNet + Subredes | - | ~$0 | -| Private DNS Zones | - | ~$1-5 | -| NSGs | - | ~$0 | -| **TOTAL ESTIMADO** | | **~$230-290/mes** | - -### Ahorro - -| Métrica | Full | Mini | Ahorro | -|---|---|---|---| -| Costo mensual | ~$3,700 | ~$260 | **~$3,440/mes (93%)** | -| Recursos Azure | ~40+ | ~15-20 | **50% menos** | -| Tiempo deploy | ~45 min | ~20 min | **55% menos** | - -> **Nota**: Los costos de ContentFlow en sí (Container Apps, Cosmos DB, Storage, etc. que crea `azd up`) son adicionales y similares en ambos escenarios (~$100-200/mes dependiendo del uso). - ---- - -## 10. Diagrama de Arquitectura Resultante - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Resource Group: rg-mini-ailz-contentflow │ -│ │ -│ ┌───────────────────────────────────────────────────────────────────┐ │ -│ │ VNet (10.0.0.0/16) │ │ -│ │ │ │ -│ │ ┌─────────────────┐ ┌──────────────────────────────────────┐ │ │ -│ │ │ Bastion Subnet │ │ Container Apps Subnet (/23) │ │ │ -│ │ │ ┌────────────┐ │ │ │ │ │ -│ │ │ │ Bastion │ │ │ ┌──────────────────────────────┐ │ │ │ -│ │ │ │ (Basic) │ │ │ │ Container Apps Environment │ │ │ │ -│ │ │ └────────────┘ │ │ │ (Internal Load Balancer) │ │ │ │ -│ │ └─────────────────┘ │ │ │ │ │ │ │ -│ │ │ │ │ ← ContentFlow despliega: │ │ │ │ -│ │ ┌─────────────────┐ │ │ │ API / Worker / Web │ │ │ │ -│ │ │ PE Subnet │ │ │ └──────────────────────────────┘ │ │ │ -│ │ │ │ │ └──────────────────────────────────────┘ │ │ -│ │ │ ← ContentFlow │ │ │ │ -│ │ │ crea PEs: │ │ ┌──────────────────────────────────────┐ │ │ -│ │ │ ● Blob Storage │ │ │ Plataforma AILZ (módulo) │ │ │ -│ │ │ ● Cosmos DB │ │ │ │ │ │ -│ │ │ ● App Config │ │ │ ● Log Analytics Workspace │ │ │ -│ │ │ ● ACR │ │ │ ● Key Vault (GenAI) │ │ │ -│ │ │ ● AI Foundry │ │ │ ● Container Registry (GenAI) │ │ │ -│ │ └─────────────────┘ │ │ ● Storage Account (GenAI, LRS) │ │ │ -│ │ │ │ ● App Configuration (GenAI) │ │ │ -│ └───────────────────────│ │ ● Cosmos DB (GenAI) │ │ │ -│ │ │ ● NSGs │ │ │ -│ ┌────────────────────┐ │ └──────────────────────────────────────┘ │ │ -│ │ Private DNS Zones │ │ │ │ -│ │ (auto-vinculadas) │ └────────────────────────────────────────────┘ │ -│ │ │ │ -│ │ ● privatelink.blob.core.windows.net │ -│ │ ● privatelink.documents.azure.com │ -│ │ ● privatelink.azconfig.io │ -│ │ ● privatelink.azurecr.io │ -│ │ ● privatelink.cognitiveservices.azure.com │ -│ │ ● privatelink.azurecontainerapps.io │ -│ │ ● (+ otras del módulo: vault, openai, etc.) │ -│ └────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────────────┐ -│ Resource Group: rg-contentflow-ailz (creado por azd up) │ -│ │ -│ ● Storage Account (Blob + Queue) + Private Endpoints en pe-subnet │ -│ ● Cosmos DB + Private Endpoint en pe-subnet │ -│ ● App Configuration + Private Endpoint en pe-subnet │ -│ ● ACR Premium + Private Endpoint en pe-subnet │ -│ ● AI Foundry (Cognitive Services) + Private Endpoint en pe-subnet │ -│ ● Container Apps (API, Worker, Web) → Container Apps Environment │ -│ ● Application Insights → Log Analytics Workspace (compartido) │ -│ ● User Assigned Managed Identity + Role Assignments │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 11. Notas de Operación y Troubleshooting - -### 11.1 Nombres de Subredes - -El módulo AVM genera nombres de subredes internamente. ContentFlow espera `pe-subnet` y `aca-env-subnet`. Si los nombres generados por el módulo no coinciden, puedes: - -1. **Verificar nombres reales** después del apply: - ```bash - terraform output subnets - ``` - -2. **Configurar en ContentFlow** los nombres reales: - ```bash - azd env set PRIVATE_ENDPOINT_SUBNET_NAME "" - azd env set CONTAINER_APPS_SUBNET_NAME "" - ``` - -3. **Override en vnet_definition** (si el módulo lo permite): - ```hcl - vnet_definition = { - address_space = ["10.0.0.0/16"] - subnets = { - pe-subnet = { - name = "pe-subnet" - address_prefix = "10.0.1.0/27" - } - aca-env-subnet = { - name = "aca-env-subnet" - address_prefix = "10.0.16.0/23" - } - } - } - ``` - -### 11.2 Container Apps Environment Dual - -Si el módulo crea un Container Apps Environment pero ContentFlow también intenta crear uno durante `azd up`, podrías terminar con dos CAEs. Para evitar esto: - -- **Opción A**: Configurar ContentFlow para usar el CAE existente (pasar su ID como variable de entorno) -- **Opción B**: Deshabilitar el CAE en el módulo (`deploy = false` en `container_app_environment_definition`) y dejar que ContentFlow lo cree - -### 11.3 Error: "Address space conflict" - -Si ves errores sobre conflictos de rango de IP: - -``` -Error: creating Virtual Network: the address space "192.168.0.0/20" overlaps with... -``` - -Asegúrate de usar un rango que **no esté en uso** en tu suscripción. Recomendamos `10.0.0.0/16` en lugar de `192.168.0.0/x`. - -### 11.4 Error: "Insufficient subnet size" - -Container Apps Environment requiere una subred de al menos `/23` (512 IPs). El módulo normalmente calcula esto correctamente, pero si haces override de subredes, asegúrate de respetar este mínimo. - -### 11.5 Terraform Destroy: Orden de Dependencias - -Para destruir la Mini-AILZ: - -```bash -# PRIMERO: Destruir ContentFlow (tiene dependencias en la red) -cd /path/to/contentflow -azd down --force --purge - -# SEGUNDO: Destruir la Mini-AILZ -cd /path/to/mini-ailz -terraform destroy -``` - -> **Importante**: Si intentas destruir la Mini-AILZ mientras ContentFlow tiene Private Endpoints activos en las subredes, el destroy fallará. Siempre destruye ContentFlow primero. - -### 11.6 Restricción de Address Space para Foundry CapabilityHost - -Si alguna vez necesitas habilitar AI Foundry con agent service (`capabilityHost`), hay una restricción conocida: - -> El address space de la VNet **no puede ser** `192.168.0.0/16` (pero sí puede ser ranges dentro de él como `192.168.0.0/20`). Otros rangos RFC1918 como `10.0.0.0/8` y `172.16.0.0/12` **sí funcionan correctamente**. - -Nuestro rango `10.0.0.0/16` no tiene este problema. - ---- - -## 12. Limpieza de Recursos - -### Limpieza Completa (ContentFlow + Mini-AILZ) - -```bash -# 1. Destruir ContentFlow primero -cd /path/to/contentflow -azd down --force --purge - -# 2. Destruir Mini-AILZ -cd /path/to/mini-ailz -terraform destroy -auto-approve - -# 3. Verificar que el RG fue eliminado -az group show --name rg-mini-ailz-contentflow 2>/dev/null && echo "RG aún existe" || echo "RG eliminado" -``` - -### Limpieza Solo ContentFlow (mantener Mini-AILZ) - -```bash -# Solo destruir ContentFlow, la red permanece para reusar -cd /path/to/contentflow -azd down --force --purge -``` - -### Purge de Recursos con Soft-Delete - -Algunos recursos Azure tienen soft-delete. Si necesitas re-crear con el mismo nombre: - -```bash -# Key Vault -az keyvault purge --name - -# Cognitive Services -az cognitiveservices account purge \ - --location \ - --resource-group \ - --name - -# App Configuration -az appconfig purge --name -``` - ---- - -## Referencia Rápida - -### Versiones Utilizadas - -| Componente | Versión | -|---|---| -| Terraform | >= 1.9, < 2.0 | -| AVM Pattern Module | 0.4.0 | -| azurerm provider | >= 3.116, < 5.0 | -| azapi provider | ~> 2.0 | -| ContentFlow | Modo `ailz-integrated` | - -### Documentos Relacionados - -- [01-overview.md](01-overview.md) — Visión general de ContentFlow -- [02-architecture-detailed.md](02-architecture-detailed.md) — Arquitectura técnica detallada -- [03-use_cases_examples.md](03-use_cases_examples.md) — Casos de uso y ejemplos -- [04-ai_lz_options.md](04-ai_lz_options.md) — Guía de despliegue AILZ (Azure CLI manual) -- **[Módulo AVM en GitHub](https://github.com/Azure/terraform-azurerm-avm-ptn-aiml-landing-zone)** — Código fuente y documentación completa -- **[Módulo AVM en Terraform Registry](https://registry.terraform.io/modules/Azure/avm-ptn-aiml-landing-zone/azurerm/latest)** — Registry oficial - ---- - -> **Última actualización**: Julio 2025 -> **Módulo fuente**: `Azure/avm-ptn-aiml-landing-zone/azurerm` v0.4.0 -> **Autor**: Análisis técnico para demos ContentFlow con suscripción única diff --git a/Analysis/06-service-health.md b/Analysis/06-service-health.md deleted file mode 100644 index 719b6de..0000000 --- a/Analysis/06-service-health.md +++ /dev/null @@ -1,825 +0,0 @@ -# Validación de Salud de Servicios en ContentFlow - -> **Documento técnico** — Arquitectura completa del sistema de health checks, flujo de validación servicio por servicio, y configuraciones necesarias para que funcione correctamente en modo `ailz-integrated` (conectividad privada). - ---- - -## Tabla de Contenidos - -1. [Resumen del Sistema de Health Checks](#1-resumen-del-sistema-de-health-checks) -2. [Arquitectura del Flujo de Validación](#2-arquitectura-del-flujo-de-validación) -3. [Componente Web: Footer y Diálogo de System Health](#3-componente-web-footer-y-diálogo-de-system-health) -4. [API: Router de Health y Endpoint `/api/health`](#4-api-router-de-health-y-endpoint-apihealth) -5. [API: HealthService — Lógica de Validación por Servicio](#5-api-healthservice--lógica-de-validación-por-servicio) - - [5.1 App Configuration](#51-app-configuration) - - [5.2 Cosmos DB](#52-cosmos-db) - - [5.3 Blob Storage](#53-blob-storage) - - [5.4 Storage Queue](#54-storage-queue) - - [5.5 Worker Engine](#55-worker-engine) -6. [Worker: Validación de Startup y Endpoint `/status`](#6-worker-validación-de-startup-y-endpoint-status) -7. [Container Apps: Liveness Probes (Infraestructura)](#7-container-apps-liveness-probes-infraestructura) -8. [Configuración de la Variable `VITE_API_BASE_URL`](#8-configuración-de-la-variable-vite_api_base_url) -9. [Diagnóstico en Modo AILZ-Integrated](#9-diagnóstico-en-modo-ailz-integrated) - - [9.1 Problema: Todos los Servicios Aparecen Offline](#91-problema-todos-los-servicios-aparecen-offline) - - [9.2 Problema: API Conectada pero Servicios Backend en Error](#92-problema-api-conectada-pero-servicios-backend-en-error) - - [9.3 Problema: Worker Engine en Error](#93-problema-worker-engine-en-error) -10. [Requisitos de Conectividad por Servicio](#10-requisitos-de-conectividad-por-servicio) -11. [Configuraciones Requeridas para AILZ](#11-configuraciones-requeridas-para-ailz) -12. [Comandos de Verificación Manual](#12-comandos-de-verificación-manual) - ---- - -## 1. Resumen del Sistema de Health Checks - -ContentFlow implementa un sistema de validación de salud en **tres niveles**: - -| Nivel | Componente | Propósito | Frecuencia | -|---|---|---|---| -| **UI (Frontend)** | `Footer.tsx` | Muestra estado visual de cada servicio al usuario | Cada 10 minutos + al cargar | -| **API (Backend)** | `HealthService` | Valida conectividad real a cada servicio Azure | Bajo demanda (llamada de la UI) | -| **Infraestructura** | Liveness Probes | Container Apps valida que los containers estén vivos | Cada 60 segundos | - -### Servicios Validados - -El sistema valida **6 componentes**: - -| Servicio | Qué Valida | Método de Validación | -|---|---|---| -| **API Service** | Que el frontend puede alcanzar el API | HTTP GET a `/api/health` | -| **App Configuration** | Conectividad al store de configuración | Listar keys con prefijo `contentflow.app.*` | -| **Cosmos DB** | Conectividad a la base de datos | Leer propiedades del database | -| **Blob Storage** | Acceso al container de documentos | Obtener propiedades del container | -| **Storage Queue** | Acceso a la cola de trabajo | Obtener propiedades de la queue | -| **Worker Engine** | Que el worker está corriendo | HTTP GET a `https://{worker-fqdn}/status` | - -### Estados Posibles - -| Estado | Indicador Visual | Significado | -|---|---|---| -| **connected** | 🟢 Verde | Todos los servicios responden correctamente | -| **degraded** | 🟡 Amarillo | Algunos servicios están con error pero no todos | -| **offline** | 🔴 Rojo | Todos los servicios con error o API inalcanzable | - ---- - -## 2. Arquitectura del Flujo de Validación - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ NAVEGADOR │ -│ │ -│ Footer.tsx │ -│ ┌─────────────────┐ │ -│ │ System Health ● │ ← Indicador visual en el footer │ -│ └────────┬────────┘ │ -│ │ click │ -│ ┌────────▼────────┐ │ -│ │ Dialog con 6 │ ← Muestra detalle por servicio │ -│ │ ServiceHealth │ │ -│ │ Items │ │ -│ └────────┬────────┘ │ -│ │ HTTP GET │ -│ │ {VITE_API_BASE_URL}/health │ -└───────────┼─────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ API Container App (api-{token}) │ -│ Puerto: 8090 │ -│ │ -│ FastAPI Router: /api/health │ -│ ┌──────────────────────────────┐ │ -│ │ health_router.get("/") │ │ -│ │ → HealthService │ │ -│ │ .check_all_services() │ │ -│ └──────────┬───────────────────┘ │ -│ │ asyncio.gather (5 checks en paralelo) │ -│ │ │ -│ ┌──────────▼───────────────────────────────────────────┐ │ -│ │ │ │ -│ │ ┌─────────────┐ ┌──────────┐ ┌───────────────┐ │ │ -│ │ │ App Config │ │ Cosmos DB│ │ Blob Storage │ │ │ -│ │ │ list keys │ │ read db │ │ get container │ │ │ -│ │ └──────┬──────┘ └────┬─────┘ └──────┬────────┘ │ │ -│ │ │ │ │ │ │ -│ │ ┌──────┴──────┐ ┌───┴────────────┐ │ │ -│ │ │ Storage Q │ │ Worker Engine │ │ │ -│ │ │ get props │ │ GET /status │ │ │ -│ │ └──────┬──────┘ └───┬────────────┘ │ │ -│ └─────────┼──────────────┼───────────────────────────────┘ │ -└────────────┼──────────────┼──────────────────────────────────────────────┘ - │ │ - ▼ ▼ -┌────────────────┐ ┌─────────────────────────────────────┐ -│ Azure Services │ │ Worker Container App (worker-{token})│ -│ (via PE en │ │ Puerto: 8099 │ -│ AILZ mode) │ │ GET /status → WorkerStatusResponse │ -└────────────────┘ └─────────────────────────────────────┘ -``` - ---- - -## 3. Componente Web: Footer y Diálogo de System Health - -**Archivo:** `contentflow-web/src/components/Footer.tsx` - -### Cómo Funciona - -1. El componente `Footer` se monta al cargar la aplicación -2. Después de **5 segundos** de espera inicial, ejecuta el primer health check -3. Luego repite el check cada **10 minutos** (600,000 ms) -4. El usuario puede forzar un refresh manual desde el diálogo - -### Flujo del Check - -```typescript -// Footer.tsx — Lógica principal -const checkHealth = async () => { - try { - const healthData = await getHealthCheck(); // GET {VITE_API_BASE_URL}/health - - // Si llegamos aquí, el API es alcanzable - setSystemHealth({ - api: "connected", - systemOverall: healthData.status, // "connected" | "degraded" | "offline" - appConfig: healthData.services.app_config?.status, - cosmosDB: healthData.services.cosmos_db?.status, - blobStorage: healthData.services.blob_storage?.status, - storageQueue: healthData.services.storage_queue?.status, - worker: healthData.services.worker?.status, - serviceDetails: { ... } // Detalles expandibles por servicio - }); - } catch (error) { - // Si el fetch falla → TODO se marca como offline - // Esto significa que el navegador no puede alcanzar el API - setSystemHealth({ api: "offline", systemOverall: "offline", ... }); - } -}; -``` - -### Qué Muestra el Diálogo - -Cada servicio se muestra como un item expandible (`ServiceHealthItem`) que incluye: - -- **Nombre del servicio** y indicador de color (verde/rojo) -- **Endpoint** consultado (expandible) -- **Tiempo de respuesta** en milisegundos -- **Mensaje** de resultado -- **Error** detallado si falló (en rojo) -- **Detalles** adicionales (JSON con metadatos del servicio) -- **Última verificación** (timestamp) - -### URL de Conexión al API - -La Web UI construye la URL del API usando la variable de entorno **`VITE_API_BASE_URL`**: - -```typescript -// apiClient.ts -const defaultConfig: ApiConfig = { - baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8090/api/', -}; -``` - -**Esto es crítico en modo AILZ** — ver [sección 8](#8-configuración-de-la-variable-vite_api_base_url). - ---- - -## 4. API: Router de Health y Endpoint `/api/health` - -**Archivo:** `contentflow-api/app/routers/health.py` - -### Endpoints Disponibles - -| Endpoint | Método | Descripción | -|---|---|---| -| `GET /api/health/` | GET | Validación completa de **todos** los servicios | -| `GET /api/health/{service_name}` | GET | Validación de un servicio específico | - -### Respuesta del Endpoint `/api/health/` - -```json -{ - "status": "connected | degraded | error", - "services": { - "app_config": { - "name": "app_config", - "status": "connected | error", - "message": "Connected successfully", - "error": null, - "details": { "config_items_count": 5, "credential_type": "azure_credential" }, - "response_time_ms": 120, - "last_checked": "2026-03-28T10:30:00Z", - "endpoint": "https://appcs-r7b6gtdiob4em.azconfig.io" - }, - "cosmos_db": { "..." }, - "blob_storage": { "..." }, - "storage_queue": { "..." }, - "worker": { "..." } - }, - "checked_at": "2026-03-28T10:30:00Z", - "summary": { "connected": 5, "error": 0, "total": 5 } -} -``` - -### Lógica de Estado General - -```python -# Determinación del estado overall -statuses = [service.status for service in services.values()] -if all(status == "connected" for status in statuses): - overall_status = "connected" # TODO bien → 🟢 -elif all(status == "error" for status in statuses): - overall_status = "error" # TODO mal → 🔴 -else: - overall_status = "degraded" # Algunos bien, algunos mal → 🟡 -``` - -### Inyección de Dependencias - -El `HealthService` se construye con los valores de configuración de App Config: - -```python -# dependencies.py -def get_health_service(): - app_settings = get_settings() - return HealthService( - cosmos_endpoint=app_settings.COSMOS_DB_ENDPOINT, - cosmos_db_name=app_settings.COSMOS_DB_NAME, - blob_storage_account=app_settings.BLOB_STORAGE_ACCOUNT_NAME, - blob_storage_container=app_settings.BLOB_STORAGE_CONTAINER_NAME, - storage_account_worker_queue_url=app_settings.STORAGE_ACCOUNT_WORKER_QUEUE_URL, - storage_worker_queue_name=app_settings.STORAGE_WORKER_QUEUE_NAME, - worker_engine_api_endpoint=app_settings.WORKER_ENGINE_API_ENDPOINT - ) -``` - -Estos valores se cargan de **Azure App Configuration** al iniciar la API, usando las keys con prefijo `contentflow.common.*` y `contentflow.api.*`. - ---- - -## 5. API: HealthService — Lógica de Validación por Servicio - -**Archivo:** `contentflow-api/app/services/health_service.py` - -Todos los checks se ejecutan **en paralelo** usando `asyncio.gather()`, lo que minimiza el tiempo de respuesta total. - ---- - -### 5.1 App Configuration - -**Método:** `_check_app_config_health()` - -**Qué valida:** -1. Que `AZURE_APP_CONFIG_ENDPOINT` o `AZURE_APP_CONFIG_CONNECTION_STRING` existan como variable de entorno -2. Que se pueda conectar al store -3. Que se puedan listar keys con filtro `contentflow.app.*` - -**Cómo se conecta:** -- Si existe `AZURE_APP_CONFIG_CONNECTION_STRING` → usa connection string -- Si existe `AZURE_APP_CONFIG_ENDPOINT` → usa `DefaultAzureCredential` (Managed Identity) - -**Qué puede fallar en AILZ:** -- App Config tiene `publicNetworkAccess: Disabled` -- La API container app necesita resolver `appcs-{token}.azconfig.io` → IP privada del Private Endpoint -- Si la Private DNS Zone `privatelink.azconfig.io` no tiene el A record correcto, falla - -**Ejemplo de error típico:** -```json -{ - "status": "error", - "message": "Connection failed: ServiceRequestError", - "error": "Cannot connect to host appcs-r7b6gtdiob4em.azconfig.io:443" -} -``` - ---- - -### 5.2 Cosmos DB - -**Método:** `_check_cosmos_db_health()` - -**Qué valida:** -1. Que `COSMOS_DB_ENDPOINT` esté configurado (no vacío) -2. Conectividad al endpoint de Cosmos DB -3. Que el database especificado (`contentflow`) exista -4. Lectura de propiedades del database (operación de solo lectura) - -**Cómo se conecta:** -- Usa `CosmosClient` con `DefaultAzureCredential` (Managed Identity del Container App) -- Endpoint: `https://cosmos-{token}.documents.azure.com:443/` - -**Qué puede fallar en AILZ:** -- Cosmos DB tiene `publicNetworkAccess: Disabled` -- Requiere Private Endpoint resolviendo a IP privada via `privatelink.documents.azure.com` -- Requiere que la Managed Identity tenga rol `Cosmos DB Built-in Data Contributor` - -**Ejemplo de error típico:** -```json -{ - "status": "error", - "message": "Connection failed: CosmosHttpResponseError", - "error": "Request blocked by network rules" -} -``` - ---- - -### 5.3 Blob Storage - -**Método:** `_check_blob_storage_health()` - -**Qué valida:** -1. Que `BLOB_STORAGE_ACCOUNT_NAME` y `BLOB_STORAGE_CONTAINER_NAME` estén configurados -2. Conectividad a la cuenta de storage -3. Que el container `content` exista -4. Lee propiedades del container (last_modified, lease_status) - -**Cómo se conecta:** -- Construye URL: `https://{account}.blob.core.windows.net` -- Usa `BlobServiceClient` con `DefaultAzureCredential` - -**Qué puede fallar en AILZ:** -- Storage tiene `publicNetworkAccess: Disabled` -- Requiere Private Endpoint blob resolviendo via `privatelink.blob.core.windows.net` -- Requiere rol `Storage Blob Data Contributor` en la Managed Identity - ---- - -### 5.4 Storage Queue - -**Método:** `_check_storage_queue_health()` - -**Qué valida:** -1. Que `STORAGE_ACCOUNT_WORKER_QUEUE_URL` esté configurado -2. Conectividad al servicio de colas -3. Que la queue `contentflow-execution-requests` exista -4. Lee propiedades de la queue (approximate_message_count) - -**Cómo se conecta:** -- Usa `QueueServiceClient` con `DefaultAzureCredential` -- URL: `https://{account}.queue.core.windows.net` - -**Qué puede fallar en AILZ:** -- Requiere Private Endpoint queue resolviendo via `privatelink.queue.core.windows.net` -- Si la DNS Zone Group del queue PE no se creó correctamente, no hay A record -- Requiere rol `Storage Queue Data Contributor` - -**Nota importante:** La Private DNS Zone para queue (`privatelink.queue.core.windows.net`) puede no haberse pasado al Bicep si `existingQueuePrivateDnsZoneId` no estaba en `main.parameters.json`. Es un gap conocido — ver [sección 11](#11-configuraciones-requeridas-para-ailz). - ---- - -### 5.5 Worker Engine - -**Método:** `_check_worker_health()` - -**Qué valida:** -1. Que el Worker Container App esté corriendo y responda -2. Llama a `GET https://{worker-fqdn}/status` -3. Valida que el response tenga status 200 -4. Extrae: `running`, `worker_name`, `processing_workers`, `source_workers` - -**Cómo se conecta:** -- Usa `aiohttp` para hacer HTTP GET al FQDN interno del Worker -- Endpoint configurado en App Config: `contentflow.api.WORKER_ENGINE_API_ENDPOINT` -- Valor típico: `https://worker-{token}.internal.{domain}.azurecontainerapps.io` -- Timeout: 5 segundos - -**Qué puede fallar en AILZ:** -- El API Container App necesita resolver el FQDN interno del Worker Container App -- En AILZ, los FQDNs tienen formato `*.internal.{defaultDomain}`, resolubles solo dentro del CAE o del VNet con Private DNS Zone -- La comunicación entre Container Apps **dentro del mismo CAE** funciona sin necesidad de DNS Zone externa — el CAE resuelve internamente -- Si la key `WORKER_ENGINE_API_ENDPOINT` en App Config tiene un FQDN incorrecto, falla - -**Ejemplo de error típico:** -```json -{ - "status": "error", - "message": "Worker health check failed: ClientConnectorError", - "error": "Cannot connect to host worker-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io:443" -} -``` - ---- - -## 6. Worker: Validación de Startup y Endpoint `/status` - -**Archivos:** `contentflow-worker/app/startup.py`, `contentflow-worker/app/api.py` - -### Validación al Iniciar (StartupValidator) - -Antes de aceptar trabajo, el Worker ejecuta 4 validaciones: - -| Check | Qué Valida | Consecuencia si Falla | -|---|---|---| -| **Settings Validation** | Que todas las configuraciones requeridas existen | Worker no inicia | -| **Cosmos DB Connectivity** | Conexión al endpoint, database existe | Worker no inicia | -| **Cosmos DB Containers** | Que los 5 containers requeridos existen | Worker no inicia | -| **Storage Queue Connectivity** | Conexión a la queue, queue existe | Worker no inicia | - -Si cualquier check falla, el Worker **no inicia** y loguea los errores detallados. - -### Health Endpoints del Worker - -| Endpoint | Descripción | -|---|---| -| `GET /` | Info básica del servicio | -| `GET /health` | Health check simple (solo verifica que el API responde) | -| `GET /status` | Estado detallado del engine (workers running, counts) | - -El endpoint `/status` es el que usa el API `HealthService` para validar el Worker. - -### Monitoreo Interno de Workers - -El `WorkerEngine` tiene un loop que cada **30 segundos** verifica si los worker processes siguen vivos: - -```python -def _check_worker_health(self): - # Si un processing worker murió, lo reinicia - for i, worker in enumerate(self.processing_workers): - if not worker.is_alive() and not self.stop_event.is_set(): - new_worker = self._create_processing_worker(i) - new_worker.start() - - # Lo mismo para source workers -``` - ---- - -## 7. Container Apps: Liveness Probes (Infraestructura) - -**Archivo:** `infra/bicep/modules/container-app.bicep` - -### Configuración del Liveness Probe - -Cada Container App tiene un probe HTTP configurado por Bicep: - -```bicep -probes: [ - { - type: 'Liveness' - httpGet: { - path: livenessProbePath // '/' para los 3 servicios - port: targetPort // 8090 (API), 8099 (Worker), 8080 (Web) - } - initialDelaySeconds: 5 // Espera 5s antes del primer check - periodSeconds: 60 // Verifica cada 60 segundos - } -] -``` - -| Container App | Probe Path | Puerto | Qué Responde | -|---|---|---|---| -| API (`api-{token}`) | `/` | 8090 | FastAPI root endpoint | -| Worker (`worker-{token}`) | `/` | 8099 | FastAPI root endpoint | -| Web (`web-{token}`) | `/` | 8080 | nginx sirve `index.html` | - -### Diferencia con el Health Check de la UI - -- **Liveness Probe**: Lo ejecuta Azure Container Apps. Si falla 3 veces consecutivas, reinicia el container. Solo verifica que el proceso responde. -- **Health Check de la UI**: Lo ejecuta el navegador del usuario → API → servicios backend. Verifica conectividad a cada servicio Azure. - ---- - -## 8. Configuración de la Variable `VITE_API_BASE_URL` - -Esta variable es **el puente fundamental** entre la Web UI y el API. Si está mal configurada, todos los servicios aparecen offline. - -### Cómo se Configura - -**En Bicep** (al crear el Web Container App): -```bicep -// main.bicep, línea ~663 -environmentVariables: [ - { - name: 'VITE_API_BASE_URL' - value: 'https://${apiContainerApp.outputs.fqdn}/api/' - } -] -``` - -**En el Dockerfile** (runtime replacement): -```dockerfile -# Build time: se escribe placeholder -RUN echo "VITE_API_BASE_URL=API_URL" > .env - -# Runtime: entrypoint.sh reemplaza el placeholder en los JS compilados -sed -i "s|API_URL|${VITE_API_BASE_URL:-}|g" "$file" -``` - -### Valor en Modo AILZ - -En modo `ailz-integrated`, el FQDN del API tiene formato interno: -``` -https://api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/api/ -``` - -**El problema:** Este FQDN solo es resolvible dentro del VNet (o si existe la Private DNS Zone del CAE). Cuando el usuario abre la Web UI en su navegador: - -1. El **navegador** (corriendo en el JumpBox o máquina del usuario) intenta hacer `fetch()` a esa URL -2. La resolución DNS de ese FQDN debe funcionar **desde la máquina del navegador** -3. Si la Private DNS Zone para `delightfulwave-2b288efe.eastus2.azurecontainerapps.io` no existe o no está vinculada al VNet → DNS falla → todo aparece offline - -### Con Application Gateway - -Cuando hay un App Gateway como intermediario, el flujo cambia: - -``` -Navegador → App Gateway IP → /api/* → API Container App (interno) -``` - -En este escenario, `VITE_API_BASE_URL` debería apuntar al **App Gateway**, no directamente al API Container App. Esto requiere: - -1. El App Gateway tiene una IP (pública o privada) accesible desde el navegador -2. La Web UI está configurada para llamar al App Gateway -3. El App Gateway hace path-based routing: `/api/*` → API backend - -**Opciones de configuración:** - -| Escenario | `VITE_API_BASE_URL` | Funciona Desde | -|---|---|---| -| AILZ sin App Gateway | `https://api-{token}.internal.{domain}/api/` | Solo JumpBox (si DNS resuelve) | -| AILZ con App Gateway (IP privada) | `https://{appgw-private-ip}/api/` o dominio custom | Dentro del VNet | -| AILZ con App Gateway (IP pública) | `https://{dominio-publico}/api/` | Cualquier lugar | -| Relativo (recomendado con AppGW) | `/api/` | Donde sea que el usuario acceda | - -**Recomendación para AILZ con App Gateway:** Usar path relativo `/api/` como `VITE_API_BASE_URL`, así el navegador usa el mismo host por el que accedió a la web. Esto funciona porque el App Gateway enruta `/*` → Web y `/api/*` → API. - ---- - -## 9. Diagnóstico en Modo AILZ-Integrated - -### 9.1 Problema: Todos los Servicios Aparecen Offline - -**Síntoma:** El indicador en el footer está 🔴, el diálogo muestra todo en rojo. - -**Causa más probable:** El navegador **no puede alcanzar** el API Container App. - -**Verificación:** - -```bash -# Desde el JumpBox, verificar que el FQDN del API resuelve -nslookup api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io - -# Si NO resuelve → falta Private DNS Zone para el dominio del CAE -# Si SÍ resuelve → verificar que el API responde -curl -k https://api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/api/health -``` - -**Causa raíz:** No existe Private DNS Zone para `delightfulwave-2b288efe.eastus2.azurecontainerapps.io` (el dominio dinámico del CAE) vinculada al VNet. - -**Solución:** El equipo de plataforma debe crear: -1. Private DNS Zone: `delightfulwave-2b288efe.eastus2.azurecontainerapps.io` -2. A record: `*` → `10.0.195.200` (CAE static IP) -3. VNet link al VNet del AILZ - -**Nota:** La DNS Zone `privatelink.azurecontainerapps.io` que ya existe en el AILZ es para el **management plane** de Container Apps, no para los FQDNs de las aplicaciones. Los FQDNs de las apps usan el dominio dinámico del CAE. - ---- - -### 9.2 Problema: API Conectada pero Servicios Backend en Error - -**Síntoma:** API está 🟢, pero Cosmos DB, Storage, App Config están 🔴. - -**Causa más probable:** Los Private Endpoints no tienen A records en las DNS Zones, o la Managed Identity no tiene los roles RBAC correctos. - -**Verificación:** - -```bash -# Verificar que los A records existen en cada DNS Zone -az network private-dns record-set a list \ - -g {AILZ_RG} -z "privatelink.documents.azure.com" -o table - -az network private-dns record-set a list \ - -g {AILZ_RG} -z "privatelink.blob.core.windows.net" -o table - -az network private-dns record-set a list \ - -g {AILZ_RG} -z "privatelink.azconfig.io" -o table -``` - -**Si faltan A records:** Los DNS Zone Groups no se crearon correctamente. Los módulos en `main.bicep` incluyen DNS Zone Groups explícitos como workaround por incompatibilidades de AVM. Verificar que se ejecutaron: - -```bash -# Verificar DNS Zone Groups de private endpoints -az network private-endpoint dns-zone-group list \ - --endpoint-name "{storageAccountName}-blob-pe" \ - -g {CF_RG} -o table -``` - -**Si los A records existen pero sigue fallando:** Verificar RBAC: - -```bash -# Listar roles de la Managed Identity -az role assignment list \ - --assignee {MANAGED_IDENTITY_PRINCIPAL_ID} \ - --scope "/subscriptions/{sub}/resourceGroups/{CF_RG}" \ - -o table -``` - ---- - -### 9.3 Problema: Worker Engine en Error - -**Síntoma:** Todos los servicios están 🟢 excepto Worker Engine que está 🔴. - -**Causa más probable:** El API Container App no puede alcanzar el Worker Container App, o el valor de `WORKER_ENGINE_API_ENDPOINT` en App Config es incorrecto. - -**Verificación:** - -```bash -# Verificar qué valor tiene WORKER_ENGINE_API_ENDPOINT en App Config -az appconfig kv show \ - --name {APP_CONFIG_NAME} \ - --key "contentflow.api.WORKER_ENGINE_API_ENDPOINT" \ - --auth-mode login \ - -o tsv --query value -``` - -**Debe ser:** `https://worker-{token}.internal.{defaultDomain}` (en modo AILZ el FQDN incluye `.internal.`). - -**Si el valor es correcto:** La comunicación entre containers del mismo CAE es interna y no requiere DNS Zone externa. Verificar que el Worker está corriendo: - -```bash -# Ver logs del Worker -az containerapp logs show \ - -n worker-{token} -g {CF_RG} --type system - -# Ver si el container está healthy -az containerapp show -n worker-{token} -g {CF_RG} \ - --query "properties.runningStatus" -``` - -**Nota sobre post-provision.sh:** En modo AILZ, la key `WORKER_ENGINE_API_ENDPOINT` se configura via `post-provision.sh` que corre desde el JumpBox. Este script toma el valor de `azd env get-value WORKER_ENDPOINT` y lo sube a App Config. Si `post-provision.sh` no corrió o se saltó, esta key puede estar vacía o con un valor incorrecto. - ---- - -## 10. Requisitos de Conectividad por Servicio - -Mapa completo de lo que necesita cada health check para funcionar en modo AILZ: - -| Servicio | Endpoint que Contacta | DNS Zone Requerida | RBAC Requerido | PE Name | -|---|---|---|---|---| -| App Config | `appcs-{token}.azconfig.io` | `privatelink.azconfig.io` | `App Configuration Data Reader` | `{appcs}-pe` | -| Cosmos DB | `cosmos-{token}.documents.azure.com` | `privatelink.documents.azure.com` | `Cosmos DB Built-in Data Contributor` | `{cosmos}-pe` | -| Blob Storage | `{storage}.blob.core.windows.net` | `privatelink.blob.core.windows.net` | `Storage Blob Data Contributor` | `{storage}-blob-pe` | -| Storage Queue | `{storage}.queue.core.windows.net` | `privatelink.queue.core.windows.net` | `Storage Queue Data Contributor` | `{storage}-queue-pe` | -| Worker | `worker-{token}.internal.{domain}` | N/A (intra-CAE) | N/A | N/A | -| API (desde Web) | `api-{token}.internal.{domain}` | `{defaultDomain}` del CAE | N/A | N/A | - -### Cadena Completa para que el Health Check Funcione - -``` -1. Navegador → puede resolver FQDN del API (o IP del App Gateway) -2. Navegador → HTTPS al API container -3. API → puede resolver endpoints de cada servicio Azure via Private DNS -4. API → Managed Identity tiene roles RBAC para leer cada servicio -5. API → puede resolver FQDN del Worker (intra-CAE, automático) -6. Worker → está corriendo y respondiendo en /status -``` - -Si **cualquier** eslabón de esta cadena falla, el servicio aparece en error. - ---- - -## 11. Configuraciones Requeridas para AILZ - -### Checklist de Configuración - -#### Infraestructura (Bicep/equipo de plataforma) - -- [ ] Private Endpoint creado para cada servicio (Storage blob, Storage queue, Cosmos, App Config, ACR) -- [ ] DNS Zone Group existente en cada Private Endpoint (crea A record automáticamente) -- [ ] Private DNS Zones vinculadas al VNet del AILZ -- [ ] Private DNS Zone para el dominio dinámico del CAE (`{defaultDomain}`) -- [ ] Wildcard A record (`*`) apuntando al CAE static IP en la DNS Zone del CAE -- [ ] Container Apps Environment configurado como `internal: true` -- [ ] Managed Identity con roles RBAC necesarios asignados - -#### App Configuration Keys (post-provision.sh) - -Estas keys deben existir en App Config para que `HealthService` funcione: - -| Key | Valor Esperado | -|---|---| -| `contentflow.common.COSMOS_DB_ENDPOINT` | `https://cosmos-{token}.documents.azure.com:443/` | -| `contentflow.common.COSMOS_DB_NAME` | `contentflow` | -| `contentflow.common.BLOB_STORAGE_ACCOUNT_NAME` | `st{token}` | -| `contentflow.common.BLOB_STORAGE_CONTAINER_NAME` | `content` | -| `contentflow.common.STORAGE_ACCOUNT_WORKER_QUEUE_URL` | `https://st{token}.queue.core.windows.net/` | -| `contentflow.common.STORAGE_WORKER_QUEUE_NAME` | `contentflow-execution-requests` | -| `contentflow.api.WORKER_ENGINE_API_ENDPOINT` | `https://worker-{token}.internal.{domain}` | - -#### Variables de Entorno del Container App - -| Container App | Variable | Valor | -|---|---|---| -| API | `AZURE_APP_CONFIG_ENDPOINT` | `https://appcs-{token}.azconfig.io` | -| API | `AZURE_CLIENT_ID` | Client ID de la Managed Identity | -| Worker | `AZURE_APP_CONFIG_ENDPOINT` | `https://appcs-{token}.azconfig.io` | -| Worker | `AZURE_CLIENT_ID` | Client ID de la Managed Identity | -| Web | `VITE_API_BASE_URL` | `https://api-{token}.internal.{domain}/api/` (o `/api/` si usa App Gateway) | - -### Gap Conocido: Queue Private DNS Zone - -El parámetro `existingQueuePrivateDnsZoneId` existe en `main.bicep` pero **no está mapeado** en `main.parameters.json`. Esto puede causar que el Private Endpoint de queue no tenga A record en la DNS Zone, haciendo que el health check de Storage Queue falle. Solución: agregar el mapeo al parameters file o configurar vía `azd env set`. - ---- - -## 12. Comandos de Verificación Manual - -### Desde el JumpBox: Verificar DNS de Todos los Servicios - -```bash -#!/bin/bash -# verify-health-dns.sh -# Ejecutar desde el JumpBox para verificar que todos los endpoints resuelven - -RESOURCE_TOKEN="r7b6gtdiob4em" -CAE_DOMAIN="delightfulwave-2b288efe.eastus2.azurecontainerapps.io" - -echo "=== Verificación DNS para Health Checks ===" - -echo "" -echo "[1/7] API Container App (Web → API)" -nslookup api-${RESOURCE_TOKEN}.internal.${CAE_DOMAIN} - -echo "" -echo "[2/7] Worker Container App (API → Worker)" -nslookup worker-${RESOURCE_TOKEN}.internal.${CAE_DOMAIN} - -echo "" -echo "[3/7] Web Container App (Navegador → Web)" -nslookup web-${RESOURCE_TOKEN}.internal.${CAE_DOMAIN} - -echo "" -echo "[4/7] App Configuration" -nslookup appcs-${RESOURCE_TOKEN}.azconfig.io - -echo "" -echo "[5/7] Cosmos DB" -nslookup cosmos-${RESOURCE_TOKEN}.documents.azure.com - -echo "" -echo "[6/7] Blob Storage" -STORAGE_NAME=$(echo "st${RESOURCE_TOKEN}" | cut -c1-24) -nslookup ${STORAGE_NAME}.blob.core.windows.net - -echo "" -echo "[7/7] Queue Storage" -nslookup ${STORAGE_NAME}.queue.core.windows.net - -echo "" -echo "=== Todos los endpoints deben resolver a IPs privadas (10.x.x.x) ===" -``` - -### Desde el JumpBox: Probar Health Check Directamente - -```bash -# Probar el endpoint de health del API -curl -sk https://api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/api/health/ | python3 -m json.tool - -# Probar un servicio específico -curl -sk https://api-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/api/health/cosmos_db | python3 -m json.tool - -# Probar el worker directamente -curl -sk https://worker-r7b6gtdiob4em.internal.delightfulwave-2b288efe.eastus2.azurecontainerapps.io/status | python3 -m json.tool -``` - -### Verificar App Config Keys - -```bash -APP_CONFIG_NAME="appcs-r7b6gtdiob4em" - -# Listar todas las keys relevantes -az appconfig kv list \ - --name $APP_CONFIG_NAME \ - --key "contentflow.*" \ - --auth-mode login \ - --query "[].{key:key, value:value}" \ - -o table -``` - -### Verificar A Records en DNS Zones - -```bash -AILZ_RG="rg-mini-ailz" # Ajustar al nombre real - -DNS_ZONES=( - "privatelink.blob.core.windows.net" - "privatelink.queue.core.windows.net" - "privatelink.documents.azure.com" - "privatelink.azconfig.io" - "privatelink.azurecr.io" -) - -for ZONE in "${DNS_ZONES[@]}"; do - echo "=== $ZONE ===" - az network private-dns record-set a list -g $AILZ_RG -z "$ZONE" \ - --query "[].{name:name, ip:aRecords[0].ipv4Address}" -o table - echo "" -done -``` - ---- - -**Última actualización:** Marzo 2026 -**Aplica a:** ContentFlow v0.1.0, modo `ailz-integrated` diff --git a/Analysis/personal-private-configurations.md b/Analysis/personal-private-configurations.md deleted file mode 100644 index 4ec4a38..0000000 --- a/Analysis/personal-private-configurations.md +++ /dev/null @@ -1,141 +0,0 @@ -# Personal Private Configurations - -> **WARNING**: This file contains environment-specific configurations. Do NOT commit to a shared/public repository. - ---- - -## Variables - -Update these values to reuse all scripts below in any environment. - -```bash -# ── Environment ── -RESOURCE_GROUP="rg-contentflow-test-001" -SUBSCRIPTION_ID="d12d19a9-0636-4951-90a4-339158fd57d8" - -# ── Container App names ── -APP_API="api-vtkhgrhgdl4w2" -APP_WEB="web-vtkhgrhgdl4w2" -APP_WORKER="worker-vtkhgrhgdl4w2" -APPS=("$APP_API" "$APP_WEB" "$APP_WORKER") - -# ── Networking ── -MY_IP=$(curl -s ifconfig.me) -RULE_NAME="allow-my-ip" - -# ── Endpoints ── -API_URL="https://api-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" -WEB_URL="https://web-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" -WORKER_URL="https://worker-vtkhgrhgdl4w2.politeflower-3403e279.eastus2.azurecontainerapps.io" -``` - ---- - -## IP Access Restrictions on Azure Container Apps - -**Date Applied**: March 16, 2026 -**Resource Group**: `rg-contentflow-test-001` -**Subscription**: `ME-MngEnvMCAP887462-gereyeso-1` (`d12d19a9-0636-4951-90a4-339158fd57d8`) -**Region**: East US 2 - -### What was configured - -All three Container Apps were restricted to accept external ingress traffic **only** from a single IP address. Any request from a different IP receives a `403 Forbidden` response. - -| Container App | Allowed IP | Rule Name | -|---------------|-----------|-----------| -| `api-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | -| `web-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | -| `worker-vtkhgrhgdl4w2` | `57.135.198.22/32` | `allow-my-ip` | - -### Service Endpoints (restricted) - -| Service | URL | -|---------|-----| -| Web UI | `$WEB_URL` | -| API | `$API_URL` | -| API Docs | `$API_URL/docs` | -| Worker | `$WORKER_URL` | - -### Why this is safe for internal communication - -Container Apps within the **same Container Apps Environment** communicate through the internal network. IP access restrictions apply only to **external** ingress traffic (from the internet). The service-to-service flow remains unaffected: - -``` -Browser (your IP) → Web UI → (browser fetches from) → API → (internal) → Worker -``` - -- **Web → API**: The React SPA runs in your browser, so API calls originate from your IP. -- **API → Worker**: Both are in the same Container Apps Environment; internal traffic bypasses IP restrictions. - ---- - -### Apply IP restriction to all apps - -```bash -for app in "${APPS[@]}"; do - echo "Restricting $app to $MY_IP ..." - az containerapp ingress access-restriction set \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --rule-name "$RULE_NAME" \ - --ip-address "$MY_IP/32" \ - --action Allow \ - --description "Allow only my IP" -done -``` - -### Update IP on all apps (when your IP changes) - -```bash -MY_IP=$(curl -s ifconfig.me) -echo "New IP: $MY_IP" - -for app in "${APPS[@]}"; do - echo "Updating $app ..." - az containerapp ingress access-restriction remove \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --rule-name "$RULE_NAME" - - az containerapp ingress access-restriction set \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --rule-name "$RULE_NAME" \ - --ip-address "$MY_IP/32" \ - --action Allow \ - --description "Allow only my IP" -done -``` - -### Remove all restrictions (make public again) - -```bash -for app in "${APPS[@]}"; do - echo "Removing restriction from $app ..." - az containerapp ingress access-restriction remove \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --rule-name "$RULE_NAME" -done -``` - -### Verify current restrictions - -```bash -for app in "${APPS[@]}"; do - echo "=== $app ===" - az containerapp ingress access-restriction list \ - -n "$app" \ - -g "$RESOURCE_GROUP" \ - --output table -done -``` - -### Quick health check - -```bash -for url in "$API_URL/health" "$WEB_URL" "$WORKER_URL"; do - echo "$url → $(curl -s -o /dev/null -w '%{http_code}' "$url")" -done -``` From 57bb1c371d94e37c520c3744aa8c287ce80cd6bc Mon Sep 17 00:00:00 2001 From: Gerardo Reyes Date: Mon, 30 Mar 2026 12:54:36 -0400 Subject: [PATCH 29/29] content_flow merge --- Changes.md | 476 ---------------------------------------------------- Untitled.md | 0 2 files changed, 476 deletions(-) delete mode 100644 Changes.md delete mode 100644 Untitled.md diff --git a/Changes.md b/Changes.md deleted file mode 100644 index 5e7a237..0000000 --- a/Changes.md +++ /dev/null @@ -1,476 +0,0 @@ -# Deployment Fixes – `azd deploy` in `ailz-integrated` mode - -## Table of Contents - -- [1. Fix: `remoteBuild: true` Incompatible with Private ACR](#1-fix-remotebuild-true-incompatible-with-private-acr) -- [2. Fix: ACR Pull Fails on Fresh Environment Due to RBAC Propagation Delay](#2-fix-acr-pull-fails-on-fresh-environment-due-to-rbac-propagation-delay) -- [3. Fix: Container Apps `ingress.external: false` Blocks VNet Traffic in Internal CAE](#3-fix-container-apps-ingressexternal-false-blocks-vnet-traffic-in-internal-cae) -- [4. Fix: Queue Private Endpoint Missing DNS A Record](#4-fix-queue-private-endpoint-missing-dns-a-record) - ---- - -## 1. Fix: `remoteBuild: true` Incompatible with Private ACR - -**Error:** - -``` -failed to login, ran out of retries: failed to set docker credentials: -Error response from daemon: Get "https://crmpuiourm56df6.azurecr.io/v2/": denied: -client with IP '104.208.200.68' is not allowed access. -``` - -Followed by the local Docker fallback also failing: - -``` -Local fallback unavailable: the docker service is not running, please start it: -exit code: 1, stderr: permission denied while trying to connect to the Docker API -at unix:///var/run/docker.sock -``` - -**Root Cause:** - -`remoteBuild: true` in `azure.yaml` causes `azd` to use ACR Tasks to build container images. ACR Tasks run from public Azure infrastructure IPs. In `ailz-integrated` mode the ACR is provisioned with `publicNetworkAccess: Disabled`, so those IPs are rejected at the network level. This is a hard incompatibility [documented by Microsoft](https://learn.microsoft.com/en-us/azure/container-registry/container-registry-private-link#use-az-acr-build-with-private-endpoint-and-private-registry): *"If you disable public network access, `az acr build` commands will fail."* - -When `azd` fell back to a local Docker build, a second issue blocked it: the JumpBox user `aiuser` was not a member of the `docker` group, causing `permission denied` on `/var/run/docker.sock`. The Docker daemon itself was running correctly. - -**Files:** `azure.yaml`, `infra/scripts/pre-provision.sh`, `infra/README.md` - ---- - -### `azure.yaml` — Remove `remoteBuild: true` from all services - -**Before:** - -```yaml -services: - api: - project: ./contentflow-api - language: py - host: containerapp - docker: - path: ./Dockerfile - context: .. - remoteBuild: true - worker: - project: ./contentflow-worker - language: py - host: containerapp - docker: - path: ./Dockerfile - context: .. - remoteBuild: true - web: - project: ./contentflow-web - language: ts - host: containerapp - docker: - path: ./Dockerfile - remoteBuild: true -``` - -**After:** - -```yaml -services: - api: - project: ./contentflow-api - language: py - host: containerapp - docker: - path: ./Dockerfile - context: .. - worker: - project: ./contentflow-worker - language: py - host: containerapp - docker: - path: ./Dockerfile - context: .. - web: - project: ./contentflow-web - language: ts - host: containerapp - docker: - path: ./Dockerfile -``` - -Removing `remoteBuild` makes `azd` always perform a local Docker build from the machine running the command. In `basic` mode the local machine pushes to the ACR public endpoint. In `ailz-integrated` mode the JumpBox is inside the AILZ VNet and can push via the ACR private endpoint. Both modes functionally equivalent; local build was already an implicit prerequisite since Docker is declared as a required tool. - ---- - -### `infra/scripts/pre-provision.sh` — Validate Docker daemon access - -**Before:** - -```bash -echo "✓ Checking Docker installation..." -if ! command -v docker &> /dev/null; then - echo "❌ Docker is not installed." - exit 1 -fi -``` - -**After:** - -```bash -echo "✓ Checking Docker installation..." -if ! command -v docker &> /dev/null; then - echo "❌ Docker is not installed." - exit 1 -fi - -echo "✓ Checking Docker daemon..." -if ! docker info &> /dev/null; then - echo "❌ Docker daemon is not running or current user cannot connect to it." - echo " For Linux: ensure dockerd is running and your user is in the 'docker' group." - echo " Run: sudo usermod -aG docker \$USER (then log out and back in)" - exit 1 -fi -``` - -The previous check only verified that the Docker CLI binary was installed, not that the daemon was reachable by the current user. Since a local Docker build is now required at deploy time, this catches the missing group membership issue early with a clear, actionable message. - ---- - -### `infra/README.md` — Update build step description - -**Before:** - -> Builds Docker images (remote build) -> Pushes to ACR -> Updates Container Apps - -**After:** - -> Builds Docker images locally (requires Docker daemon running on the machine executing `azd`) -> Pushes to ACR (via public endpoint in `basic` mode, via private endpoint in `ailz-integrated` mode — must be executed from within the AI LZ VNet, e.g. from the JumpBox VM) -> Updates Container Apps - ---- - -## 2. Fix: ACR Pull Fails on Fresh Environment Due to RBAC Propagation Delay - -**Error:** - -``` -Failed to provision revision for container app 'api-xmi4h2hmzcfbk'. -Error details: Invalid value: "crxmi4h2hmzcfbk.azurecr.io/contentflow/api-content_flow_sunday29:azd-deploy-1774789666": -unable to pull image using Managed identity -/subscriptions/.../providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-xmi4h2hmzcfbk -for registry crxmi4h2hmzcfbk.azurecr.io -``` - -**Root Cause:** - -When `azd up` runs on a fresh environment, Bicep creates the Managed Identity, the ACR, and the `AcrPull` role assignment in a single ARM deployment. ARM reports success as soon as the role assignment _resource_ exists. However, Azure RBAC propagation is **eventually consistent** — the `AcrPull` permission can take up to 10 minutes to be effective across all Azure data planes. - -`azd up` proceeds immediately from `provision` to `deploy`. During deploy, `azd` pushes the image to ACR (succeeds because push uses the CLI's own credentials), then updates the Container App revision. The Container App infrastructure tries to pull the image using the Managed Identity's `AcrPull` role, but the role has not yet propagated — resulting in the pull failure above. - -This affects **both deployment modes** (`basic` and `ailz-integrated`) because both use Managed Identity + `AcrPull` for registry access. The failure is more frequent in `ailz-integrated` mode because the private endpoint adds additional resolution latency. - -**Files:** `azure.yaml`, `infra/scripts/pre-deploy.sh` - ---- - -### `azure.yaml` — Add global `predeploy` hook - -**Before:** - -```yaml -# Global hooks -hooks: - preprovision: - shell: sh - run: ./infra/scripts/pre-provision.sh - interactive: false - continueOnError: false - - postprovision: - shell: sh - run: ./infra/scripts/post-provision.sh - interactive: false - continueOnError: false - - postdeploy: - shell: sh - run: ./infra/scripts/post-deploy.sh - interactive: true - continueOnError: false -``` - -**After:** - -```yaml -# Global hooks -hooks: - preprovision: - shell: sh - run: ./infra/scripts/pre-provision.sh - interactive: false - continueOnError: false - - postprovision: - shell: sh - run: ./infra/scripts/post-provision.sh - interactive: false - continueOnError: false - - # Run before deploying services - validates ACR pull readiness - predeploy: - shell: sh - run: ./infra/scripts/pre-deploy.sh - interactive: false - continueOnError: false - - postdeploy: - shell: sh - run: ./infra/scripts/post-deploy.sh - interactive: true - continueOnError: false -``` - -The `predeploy` hook already existed as a script (`infra/scripts/pre-deploy.sh`) but was **not wired** into `azure.yaml`. Now it runs automatically before `azd` deploys any service — both in `azd up` and standalone `azd deploy`. - ---- - -### `infra/scripts/pre-deploy.sh` — ACR Pull Readiness Gate - -**Before:** - -```bash -#!/bin/bash -# Pre-deploy hook - runs before deploying all services -set -e - -# ... basic infrastructure check only ... - -if ! azd env get-value AZURE_RESOURCE_GROUP &> /dev/null; then - echo "❌ Infrastructure not provisioned. Please run 'azd provision' first." - exit 1 -fi - -echo "✓ Infrastructure is ready for deployment" -``` - -**After:** - -The script now performs a 3-phase ACR readiness validation: - -**Phase 1 — AcrPull role visibility polling (both modes)** - -Reads `AZURE_CONTAINER_REGISTRY_NAME` and `MANAGED_IDENTITY_PRINCIPAL_ID` from `azd env` outputs. Polls `az role assignment list` every 10 seconds until `AcrPull` appears for the principal on the ACR scope. Timeout defaults to 300 seconds. If the role is already visible (e.g. second deploy on existing environment), passes immediately with no delay. - -```bash -ROLES=$(az role assignment list \ - --assignee-object-id "$IDENTITY_PRINCIPAL_ID" \ - --scope "$ACR_RESOURCE_ID" \ - --query "[?roleDefinitionName=='AcrPull'].roleDefinitionName" \ - -o tsv 2>/dev/null || echo "") -``` - -**Phase 2 — ACR Private Endpoint DNS verification (AILZ only)** - -In `ailz-integrated` mode, verifies that the ACR login server resolves to a private IP (RFC 1918 range). If it resolves to a public IP or doesn't resolve, emits a warning. This catches misconfigured `privatelink.azurecr.io` DNS zones early. - -**Phase 3 — RBAC stabilization wait (both modes)** - -After `AcrPull` is visible in the control plane, waits a configurable stabilization period (default: 60 seconds) before allowing deploy. This compensates for the gap between role assignment queryability and data plane effectiveness. - -**Configurability:** - -| Variable | Default | Override | -|---|---|---| -| `ACR_RBAC_TIMEOUT` | 300s | `ACR_RBAC_TIMEOUT=600 azd deploy` | -| `ACR_RBAC_STABILIZATION` | 60s | `ACR_RBAC_STABILIZATION=0 azd deploy` | - -On an existing environment where RBAC is already propagated, the script detects `AcrPull` immediately and only waits the stabilization period. For subsequent deploys where no RBAC change occurred, the stabilization can be set to 0. - -**Failure behavior:** - -If the timeout is reached without finding `AcrPull`, the script exits with code 1 and prints: -- The ACR name and principal ID that failed -- The elapsed wait time -- The retry command with instructions to increase timeout - ---- - -## 3. Fix: Container Apps `ingress.external: false` Blocks VNet Traffic in Internal CAE - -**Error:** - -From JumpBox or Application Gateway attempting to reach any Container App: - -``` -HTTP 404 — site not found -``` - -The Container Apps are running and healthy, but unreachable from VNet resources outside the CAE. - -**Root Cause:** - -All three Container Apps (API, Worker, Web) were provisioned with `externalIngress: !isAILZIntegrated`, which evaluates to `false` in `ailz-integrated` mode. In an **internal** Container Apps Environment (`internal: true`), the `ingress.external` flag controls VNet-level visibility: - -| `ingress.external` | Internal CAE behavior | -|---|---| -| `true` | Accessible from **any resource in the VNet** (App Gateway, JumpBox, other subnets) | -| `false` | Accessible **only from other Container Apps in the same CAE** | - -With `external: false`, the App Gateway, JumpBox, and any other VNet-hosted client receives HTTP 404 because the CAE reverse proxy refuses to route traffic from non-CAE sources. - -Critically, in an internal CAE **neither setting exposes the app to the internet** — the CAE itself has no public IP. Setting `external: true` in an internal CAE simply opens VNet-level routing, which is exactly what's needed for Application Gateway integration. - -**Impact on both deployment modes:** - -| Mode | Before | After | Net change | -|---|---|---|---| -| `basic` | `externalIngress: true` (public CAE, internet-facing) | `externalIngress: true` | **None** — already `true` | -| `ailz-integrated` | `externalIngress: false` (internal CAE, CAE-only) | `externalIngress: true` (internal CAE, VNet-accessible) | **VNet resources can now reach the apps** — no internet exposure | - -**Files:** `infra/bicep/main.bicep` - ---- - -### `main.bicep` — Change `externalIngress` to `true` for all three Container Apps - -**Before (API, line ~578):** - -```bicep -externalIngress: !isAILZIntegrated -``` - -**After:** - -```bicep -externalIngress: true -``` - -The same change was applied to: -- **API** Container App (`apiContainerApp`) -- **Worker** Container App (`workerContainerApp`) -- **Web** Container App (`webContainerApp`) - -All three previously had `externalIngress: !isAILZIntegrated`. Now all three have `externalIngress: true`. The `container-app.bicep` module default was already `true` — the override in `main.bicep` was forcing it to `false` in AILZ mode. - ---- - -## 4. Fix: Queue Private Endpoint Missing DNS A Record - -**Error:** - -Worker container crashes on startup validation: - -``` -azure.core.exceptions.HttpResponseError: Unable to resolve host queue endpoint -``` - -Or: the queue private endpoint exists but `nslookup .queue.core.windows.net` returns a public IP instead of the private endpoint IP, causing timeouts in a private-network-only environment. - -**Root Cause:** - -Three compounding gaps prevented the queue private DNS zone ID from reaching the storage module: - -1. **`main.parameters.json`** — Missing `existingQueuePrivateDnsZoneId` mapping. The `get-ailz-resources.sh` script discovers the value and sets `EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID` in the azd environment, but the parameters file never passed it to Bicep. - -2. **`main.bicep` storage module call** — The `queuePrivateDnsZoneId` parameter was never passed. The `blobPrivateDnsZoneId` was correctly passed, but the queue equivalent was missing. - -3. **`main.bicep` storageQueueDnsZoneGroup condition** — The condition `isAILZIntegrated && !empty(existingQueuePrivateDnsZoneId)` was too restrictive. Since the parameter was always empty (due to gaps 1 and 2), this fallback DNS zone group module never executed. - -Additionally, the AILZ validation block did not validate the presence of `existingQueuePrivateDnsZoneId`, so the deployment proceeded silently without the queue DNS zone. - -**Impact on both deployment modes:** - -| Mode | Before | After | Net change | -|---|---|---|---| -| `basic` | No private endpoints, public access | No private endpoints, public access | **None** — queue PE not used in basic mode | -| `ailz-integrated` | Queue PE created but no DNS A record → worker crash | Queue PE created **with** DNS A record → worker resolves correctly | **Queue endpoint resolves to private IP** | - -**Files:** `infra/bicep/main.bicep`, `infra/bicep/main.parameters.json` - ---- - -### `main.parameters.json` — Add queue private DNS zone ID mapping - -**Before:** - -```json -"existingBlobPrivateDnsZoneId": { - "value": "${EXISTING_BLOB_PRIVATE_DNS_ZONE_ID=}" -}, -"existingCosmosPrivateDnsZoneId": { -``` - -**After:** - -```json -"existingBlobPrivateDnsZoneId": { - "value": "${EXISTING_BLOB_PRIVATE_DNS_ZONE_ID=}" -}, -"existingQueuePrivateDnsZoneId": { - "value": "${EXISTING_QUEUE_PRIVATE_DNS_ZONE_ID=}" -}, -"existingCosmosPrivateDnsZoneId": { -``` - -The `=` suffix (empty default) ensures basic mode deployments pass an empty string, which is the expected no-op value. - ---- - -### `main.bicep` — Pass `queuePrivateDnsZoneId` to storage module - -**Before:** - -```bicep -blobPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.blob : '' -publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' -``` - -**After:** - -```bicep -blobPrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.blob : '' -queuePrivateDnsZoneId: isAILZIntegrated ? networkConfig.privateDnsZoneIds.queue : '' -publicNetworkAccess: isAILZIntegrated ? 'Disabled' : 'Enabled' -``` - -Uses the same pattern as `blobPrivateDnsZoneId`: pass the DNS zone ID from `networkConfig` in AILZ mode, empty string in basic mode. The `networkConfig` variable already had the `queue` key mapped to `existingQueuePrivateDnsZoneId`. - ---- - -### `main.bicep` — Simplify `storageQueueDnsZoneGroup` condition - -**Before:** - -```bicep -module storageQueueDnsZoneGroup ... = if (isAILZIntegrated && !empty(existingQueuePrivateDnsZoneId)) { -``` - -**After:** - -```bicep -module storageQueueDnsZoneGroup ... = if (isAILZIntegrated) { -``` - -The `!empty()` guard was redundant because the AILZ validation block now fails the deployment if the queue DNS zone ID is missing. Simplifying the condition makes it consistent with the `storageBlobDnsZoneGroup` module, which already used `if (isAILZIntegrated)`. - ---- - -### `main.bicep` — Add queue DNS zone validation to AILZ block - -**Before:** - -```bicep -acrPrivateDnsZoneRequired: !empty(existingAcrPrivateDnsZoneId) ?? fail(...) -containerAppsEnvPrivateDnsZoneRequired: !empty(existingContainerAppsEnvPrivateDnsZoneId) ?? fail(...) -} : {} -``` - -**After:** - -```bicep -acrPrivateDnsZoneRequired: !empty(existingAcrPrivateDnsZoneId) ?? fail(...) -containerAppsEnvPrivateDnsZoneRequired: !empty(existingContainerAppsEnvPrivateDnsZoneId) ?? fail(...) -queuePrivateDnsZoneRequired: !empty(existingQueuePrivateDnsZoneId) ?? fail('existingQueuePrivateDnsZoneId is required for ailz-integrated mode') -} : {} -``` - -This ensures AILZ deployments fail fast with a clear error message if the queue private DNS zone ID is not provided, instead of silently deploying with a broken queue endpoint. diff --git a/Untitled.md b/Untitled.md deleted file mode 100644 index e69de29..0000000