Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 92 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ The system uses a sophisticated multi-agent architecture:

**Investment Analysis Workflow:**
```
Data Preparation → [Financial Analyst, Risk Analyst, Market Analyst, Compliance Analyst]
Data Preparation → [Financial Analyst, Risk Analyst, Market Analyst, Compliance Analyst]
→ Analysis Aggregator → Investment Debate Executor → Summary Report Generator
```

Expand Down Expand Up @@ -124,7 +124,95 @@ The application uses Server-Sent Events (SSE) for real-time updates:
- **Historical Events**: New subscribers receive historical events before live updates
- **Background Processing**: Analysis runs in FastAPI background tasks while streaming events

## 📦 Prerequisites
## 🚀 One-Click Azure Deployment

Deploy the full Azure infrastructure (zero-trust topology by default — VNet, private endpoints, App Service, Cosmos DB, Storage, Azure AI Foundry, Container Registry, and AMPLS observability) directly from the Azure Portal. **No Bastion, no jumpbox, no public IPs are provisioned** — operators are expected to reach the workload from their own peered network (ExpressRoute, VPN, or hub VNet).

Pre-built ARM templates:
- [infra/bicep/main.json](infra/bicep/main.json) — primary template
- [infra/bicep/main-linux.json](infra/bicep/main-linux.json) — Linux variant (functionally identical at the network layer)

<p align="center">
<picture>
<img src="./_assets/zero-trust-architecture.png" alt="AI Investment Analysis - Private Zero-Trust Architecture" style="max-width:800px;width:100%" />
</picture>
</p>

<p align="center">
<picture>
<img src="./docs/diagrams/private_architecture.png" alt="AI Investment Analysis - Private Architecture" style="max-width:800px;width:100%" />
</picture>
</p>

> See [`_assets/ZERO_TRUST_ARCHITECTURE.md`](_assets/ZERO_TRUST_ARCHITECTURE.md) for a full breakdown of the topology above.

#### Deploy

[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Fsaadmsft%2FAgentic-AI-Investment-Analysis-Sample%2Fmain%2Finfra%2Fbicep%2Fmain.json)
[![Visualize](https://raw.githubusercontent.com/Azure/azure-quickstart-templates/master/1-CONTRIBUTION-GUIDE/images/visualizebutton.svg?sanitize=true)](http://armviz.io/#/?load=https%3A%2F%2Fraw.githubusercontent.com%2Fsaadmsft%2FAgentic-AI-Investment-Analysis-Sample%2Fmain%2Finfra%2Fbicep%2Fmain.json)

### Before you click

1. **Create (or pick) a resource group** in your target subscription — the template deploys at resource-group scope.
2. **Have a `/26` CIDR ready** for the workload VNet. The customer's network team must allocate it from a range that does **not** overlap any peered VNet. It will be split into two `/27` subnets (App Service + Private Endpoints).
3. **Have peering in place** (ExpressRoute / VPN / hub VNet) before you try to run scripts 2 + 3 — the private ACR and App Service are not reachable from the public internet.
4. **Pick locations** that have capacity for Azure AI Foundry models (e.g. `swedencentral`, `eastus2`) for `aiFoundryLocation`.

### Key parameters

| Parameter | Default | Description |
| ------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `namePrefix` | `invstdemo` | Prefix used for any resource whose name is not explicitly overridden (see below) |
| `environment` | `dev` | Environment tag (`dev`, `staging`, `prod`) |
| `location` | resource group location | Region for most resources |
| `aiFoundryLocation` | resource group location | Region for Azure AI Foundry / model deployment |
| `isPrivate` | `true` | Deploy zero-trust topology (VNet + private endpoints + private App Service). Set `false` for a public, demo-only topology. |
| `vnetAddressPrefix` | **required** | `/26` CIDR for the workload VNet (e.g. `10.123.45.0/26`). Required even when `isPrivate=false` (placeholder is fine). |

#### Custom naming convention (optional)

Every resource also accepts an explicit `*NameOverride` parameter so customers with their own CAF / corporate naming standard can plug exact names in:

| Override parameter | Resource |
| ------------------------------------- | --------------------------------------- |
| `vnetNameOverride` | Virtual Network |
| `userAssignedIdentityNameOverride` | User-Assigned Managed Identity |
| `logAnalyticsWorkspaceNameOverride` | Log Analytics workspace |
| `appInsightsNameOverride` | Application Insights |
| `amplsNameOverride` | Azure Monitor Private Link Scope |
| `storageAccountNameOverride` | Storage account (3-24 lowercase alnum) |
| `cosmosAccountNameOverride` | Cosmos DB account |
| `containerRegistryNameOverride` | Azure Container Registry |
| `appServicePlanNameOverride` | App Service Plan |
| `aiFoundryBaseNameOverride` | AI Foundry base (≤ 12 lowercase alnum) |

Any override left empty falls back to the default `<namePrefix>-<kind>-<hash>` pattern, so existing deployments are unaffected. A worked example is in [`infra/bicep/main.investcorp.example.bicepparam`](infra/bicep/main.investcorp.example.bicepparam).

> **Note:** The portal one-click flow provisions the Azure infrastructure only. After the deployment finishes, build and push the container images and roll out the apps with the helper scripts — **run them from a workstation peered to the workload VNet**:
>
> ```bash
> ./infra/2-build-and-push-images.sh -r <your-acr-login-server>
> ./infra/3-deploy-apps.sh -g <your-resource-group>
> ```
>
> See [`infra/1-deploy-azure-infra.sh`](infra/1-deploy-azure-infra.sh) for the equivalent CLI-based deployment with all available flags, and [`_assets/ZERO_TRUST_ARCHITECTURE.md`](_assets/ZERO_TRUST_ARCHITECTURE.md) for the full network topology.

### 📘 Full private-deployment documentation

Two references are maintained:

- [`docs/PRIVATE_DEPLOYMENT.md`](docs/PRIVATE_DEPLOYMENT.md) — engineering-grade reference. Every parameter, module, subnet, Private DNS zone, RBAC assignment, app setting, and operational runbook.
- [`docs/CUSTOMER_DEPLOYMENT_INVESTCORP.md`](docs/CUSTOMER_DEPLOYMENT_INVESTCORP.md) — **customer-facing deployment package** (InvestCorp template, reusable for any customer): resource inventory + SKUs, monthly cost estimate, network requirements, operator workstation prerequisites, outbound URL whitelist for bootstrap, temporary changes required during bootstrap (e.g. `AcrPush` RBAC, optional ACR public toggle), RBAC summary, step-by-step runbook, post-deploy verification, hand-off checklist, and instructions for plugging in a custom naming convention.

Use them when you need to:

- Customize the `/26` split or NSG rules
- Understand which roles are granted to the workload UAMI and the deployer
- Switch between `isPrivate=true` (zero-trust) and `isPrivate=false` (public demo)
- Set up DNS forwarding from your peered network so private FQDNs resolve correctly
- Troubleshoot private-endpoint, DNS, or image-pull issues

## �📦 Prerequisites

- **Python 3.11+** (3.13 recommended)
- **Node.js 18+** and npm
Expand Down Expand Up @@ -225,8 +313,8 @@ npm run dev
1. Create a Cosmos DB account with NoSQL API
2. The application will automatically create the database and containers on first run
3. Ensure your connection endpoint is in the `.env` file
4. Ensure proper access permissions on Cosmos DB account:
4. Ensure proper access permissions on Cosmos DB account:

Follow the steps in this article: [Connect to Azure Cosmos DB for NoSQL using role-based access control and Microsoft Entra ID](https://learn.microsoft.com/en-us/azure/cosmos-db/nosql/how-to-connect-role-based-access-control?pivots=azure-cli)

**Blob Storage:**
Expand Down
148 changes: 148 additions & 0 deletions _assets/ZERO_TRUST_ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Zero-Trust Architecture

End-to-end view of the Agentic AI Investment Analysis sample deployed with `isPrivate=true`. Every PaaS data plane is reached through a Private Endpoint inside a customer-owned VNet; there is no public DNS record for any workload. There is **no public ingress on the workload** — operators connect from the customer's own peered network (ExpressRoute, VPN, or hub VNet).

## Logical view

See the rendered diagram in [zero-trust-architecture.mmd](zero-trust-architecture.mmd). Inline source:

```mermaid
flowchart LR
classDef pub fill:#ffe0e0,stroke:#cc0000,color:#000
classDef vnet fill:#e8f0ff,stroke:#1f4e9d,color:#000
classDef pe fill:#fff4cc,stroke:#b58900,color:#000
classDef app fill:#d6f5d6,stroke:#2e7d32,color:#000
classDef data fill:#f0e6ff,stroke:#6a1b9a,color:#000
classDef obs fill:#e0f7fa,stroke:#006064,color:#000
classDef id fill:#fde7f3,stroke:#ad1457,color:#000

Op([Operator / Developer<br/>on peered network]):::vnet
Internet([Public internet]):::pub

subgraph RG[Azure Resource Group]
subgraph VNet["Workload VNet · customer-supplied /26"]
direction TB
subgraph S_Svc["snet-services /27 · delegated Microsoft.Web/serverFarms"]
VnetInteg[App Service VNet integration<br/>all egress routed to VNet]:::vnet
end
subgraph S_Pe[snet-pe /27]
PE_Api((PE · API App)):::pe
PE_Web((PE · Web App)):::pe
PE_Acr((PE · ACR)):::pe
PE_Cos((PE · Cosmos)):::pe
PE_Blob((PE · Blob)):::pe
PE_Ai((PE · AI Foundry)):::pe
PE_Ampls((PE · AMPLS)):::pe
end
end

subgraph PaaS[Private PaaS · publicNetworkAccess = Disabled]
APIApp[API App Service<br/>DOCKER · public=Disabled]:::app
WebApp[Web App Service<br/>DOCKER · public=Disabled]:::app
ACR[Azure Container Registry<br/>Premium · admin disabled]:::data
Cosmos[Cosmos DB<br/>disableLocalAuth=true]:::data
Storage[Storage Account<br/>allowSharedKeyAccess=false]:::data
AI[Azure AI Foundry<br/>+ OpenAI gpt-4.1-mini]:::data
end

subgraph Obs[Observability via AMPLS]
LA[Log Analytics<br/>ingestion/query private]:::obs
AppI[Application Insights<br/>disableLocalAuth=true]:::obs
AMPLS[Azure Monitor<br/>Private Link Scope]:::obs
end

subgraph Identity
UAMI[User-Assigned Managed Identity<br/>AcrPull/Push · Storage Blob · Cosmos Data Contributor · Azure AI User]:::id
end

PDNS[(Private DNS Zones<br/>· azurewebsites.net<br/>· documents.azure.com<br/>· blob.core.windows.net<br/>· azurecr.io<br/>· openai / cognitiveservices / services.ai<br/>· monitor / oms / ods / agentsvc)]:::vnet
end

%% Operator path — via customer peering
Op -- HTTPS via peering --> PE_Web
Op -- docker push / az deploy --> PE_Acr

%% App egress via VNet integration
WebApp -- VNet integration --> VnetInteg
APIApp -- VNet integration --> VnetInteg
VnetInteg -- AAD token --> PE_Cos
VnetInteg -- AAD token --> PE_Blob
VnetInteg -- AAD token --> PE_Ai
VnetInteg -- image pull --> PE_Acr

%% Private Endpoints map to PaaS
PE_Api -. private link .-> APIApp
PE_Web -. private link .-> WebApp
PE_Acr -. private link .-> ACR
PE_Cos -. private link .-> Cosmos
PE_Blob -. private link .-> Storage
PE_Ai -. private link .-> AI
PE_Ampls -. private link .-> AMPLS
AMPLS --- LA
AMPLS --- AppI
APIApp -. telemetry over AMPLS .-> PE_Ampls
WebApp -. telemetry over AMPLS .-> PE_Ampls

%% DNS resolution
VnetInteg -. DNS .-> PDNS
Op -. DNS via peering .-> PDNS

%% Identity attachments
UAMI -. federated on .-> APIApp
UAMI -. federated on .-> WebApp

%% Public boundary
Internet -- blocked · no DNS --> ACR
Internet -- blocked · no DNS --> Cosmos
Internet -- blocked · no DNS --> Storage
Internet -- blocked · no DNS --> AI
Internet -- blocked · no DNS --> APIApp
Internet -- blocked · no DNS --> WebApp
```

## Request paths

### Operator deploy flow
1. Operator workstation sits on a network that is **peered to the workload VNet** (ExpressRoute, site-to-site VPN, or hub VNet). DNS for `*.privatelink.azurecr.io`, `*.privatelink.azurewebsites.net`, etc. resolves to the workload's private endpoints via the linked private DNS zones.
2. From the workstation:
- `docker push` to the private **ACR** via PE (`privatelink.azurecr.io`), **or** `az acr build` (ACR Tasks).
- `az deployment group create` for the API / Web app Bicep templates.
3. App Service control plane validates + deploys; image pull happens over the ACR private link from the App Service VNet integration subnet.

### Application runtime flow
1. Caller (peered network) reaches the **Web app**'s private FQDN (`<name>.azurewebsites.net`) which resolves to the inbound private endpoint in `snet-pe`.
2. Web app calls the **API app** via its private endpoint over the VNet.
3. API app requests an Entra ID token via the mounted UAMI (`AZURE_CLIENT_ID`) and calls:
- **Cosmos DB** → PE `Sql` · zone `privatelink.documents.azure.com`
- **Storage blob** → PE `blob` · zone `privatelink.blob.<storage-suffix>`
- **Azure OpenAI / AI Foundry** → PE `account` · zones `openai`, `cognitiveservices`, `services.ai`
4. Telemetry emits to App Insights / Log Analytics through the **AMPLS** private endpoint (`privatelink.monitor.azure.com` + `oms` + `ods` + `agentsvc`).

## Subnet layout

The customer supplies a single **/26** (64 IPs) for the workload VNet. It is split into two equal /27 subnets via `cidrSubnet()`:

| Subnet | CIDR | Purpose |
| --------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `snet-services` | /27 (offset 0) | Delegated to `Microsoft.Web/serverFarms` — App Service VNet integration; `Microsoft.CognitiveServices` service endpoint for AI Foundry |
| `snet-pe` | /27 (offset 32) | All Private Endpoints — App Service inbound, ACR, Cosmos, Blob, AI Foundry, AMPLS |

> Sizing note: /27 yields ~27 usable IPs per subnet. The App Service VNet integration subnet needs roughly 2× the worst-case instance count. If autoscale beyond ~10 instances per plan is expected, request a larger CIDR from the customer.

## Zero-trust controls checklist

| Control | Enforced at |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| No public data-plane access | `publicNetworkAccess=Disabled` on App Service apps, Cosmos, Storage, ACR, AI Foundry, Log Analytics, App Insights |
| No shared-key / local auth | `allowSharedKeyAccess=false` (Storage), `disableLocalAuthentication=true` (Cosmos), `adminUserEnabled=false` (ACR), `disableLocalAuth=true` (LA, AppI, AI Foundry) |
| Managed-identity-only workload auth | UAMI with scoped AcrPull/Push, Storage Blob Data Contributor, Cosmos Data Contributor, Azure AI User |
| Private app ingress | App Service `publicNetworkAccess=Disabled`; reachable only via private endpoints in `snet-pe` |
| Restricted CORS | `ALLOW_ORIGINS` env-driven (no `*` in private mode) |
| Private DNS | All PaaS resolution via customer zones linked to the VNet |
| Telemetry isolation | App Insights + Log Analytics scoped to an AMPLS with `PrivateOnly` ingestion + query |
| NSGs | `snet-pe` permits inbound 443 from VirtualNetwork only; `snet-services` permissive within VNet for App Service integration |
| No public surface | No Bastion, no jumpbox, no public IPs — operator access requires customer peering |

## Dual-mode (`isPrivate` flag)

The same Bicep can also deploy the original public demo topology by passing `isPrivate=false` to [`main.bicep`](../infra/bicep/main.bicep) (a placeholder `vnetAddressPrefix` like `10.0.0.0/26` is still required by the parameter signature but is unused). In this mode there is no VNet, no private endpoints, and no AMPLS — useful for quick demos but not for production.
Loading