Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e7c27d7
refactor(terraform): refactor to use less variables #94301
jimmykurian Oct 9, 2025
c1d7b14
docs(guide): update guide for terraform updates #93401
jimmykurian Oct 10, 2025
a517f7e
chore(readme): clean up redundancies #94301
jimmykurian Oct 10, 2025
a174a87
Merge branch 'main' into feat/94301
jimmykurian Oct 10, 2025
b378ef5
chore(docs): run linter #94301
jimmykurian Oct 10, 2025
ce5b2c6
chore(terraform): update terraform examples file #94301
jimmykurian Oct 10, 2025
3d63c5f
Update guides/implement_ai_foundry_basic_with_azure_function_integrat…
jimmykurian Oct 10, 2025
cc197b7
refactor(terraform): add feedback from copilot #94301
jimmykurian Oct 10, 2025
fe2f807
Merge branch 'main' into feat/94301
jimmykurian Oct 11, 2025
ce5a236
refactor(tests): update acceptance tests from PR Tests #94301
jimmykurian Oct 13, 2025
302e23b
Merge branch 'main' into feat/94301
jimmykurian Oct 13, 2025
fac5dcd
Merge branch 'main' into feat/94301
jimmykurian Oct 13, 2025
df79ada
refactor(terraform): update from PR feedback #94301
jimmykurian Oct 13, 2025
28cc3c1
refactor(terraform): update from PR Feedback #94301
jimmykurian Oct 14, 2025
9690a35
refactor(terraform): update to use provider function for parsing #94301
jimmykurian Oct 14, 2025
f6780fb
Merge branch 'main' into feat/94301
jimmykurian Oct 14, 2025
8221f07
refactor(terraform): update to use provider function for parsing #94301
jimmykurian Oct 15, 2025
5635632
docs(guide): update readme #94301
jimmykurian Oct 15, 2025
9ad2fdc
refactor(terraform): update from PR feedback #94301
jimmykurian Oct 15, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This guide shows you how to use CAIRA's `foundry_basic` reference architecture w
**What You'll Learn:**

- How to consume foundry_basic outputs in your function layer
- Automatic resource discovery through ID parsing
- Managed identity patterns for keyless authentication with AI Foundry
- Agent lifecycle management through serverless endpoints
- Monitoring integration with Application Insights
Expand Down Expand Up @@ -42,16 +43,34 @@ This guide demonstrates a clean separation of concerns:

1. **Foundation Layer** (foundry_basic): Manages AI Foundry infrastructure
1. **Function Layer** (this guide): Consumes foundry_basic outputs and provides application endpoints
1. **Integration Layer**: Terraform data sources and managed identity RBAC
1. **Integration Layer**: Terraform data sources with automatic resource discovery

**Key Integration Point:**

```hcl
# Grant function access using managed identity
resource "azurerm_role_assignment" "function_ai_foundry_user" {
scope = var.foundry_ai_foundry_id # From foundry_basic output
role_definition_name = "Cognitive Services User"
principal_id = azurerm_linux_function_app.main.identity[0].principal_id
variable "foundry_ai_foundry_id" {
type = string
description = "The resource ID of the AI Foundry account from foundry_basic deployment"
}

variable "foundry_ai_foundry_project_id" {
type = string
description = "The resource ID of the AI Foundry Project from foundry_basic deployment"
}

variable "foundry_ai_foundry_project_name" {
type = string
description = "The name of the AI Foundry project from foundry_basic deployment"
}

variable "foundry_application_insights_id" {
type = string
description = "The resource ID of the Application Insights instance from foundry_basic deployment"
}

variable "foundry_log_analytics_workspace_id" {
type = string
description = "The resource ID of the Log Analytics workspace from foundry_basic deployment"
}
```

Expand All @@ -77,8 +96,8 @@ identity {
type = "SystemAssigned" # Azure creates and manages the identity
}
app_settings = {
"AI_FOUNDRY_ENDPOINT" = var.foundry_ai_foundry_endpoint
"AI_FOUNDRY_PROJECT_NAME" = var.foundry_ai_foundry_project_name
"AI_FOUNDRY_ENDPOINT" = local.ai_foundry_endpoint # Discovered via data source
"AI_FOUNDRY_PROJECT_NAME" = local.ai_foundry_project_name # From foundry_basic output
"AI_FOUNDRY_PROJECT_ID" = var.foundry_ai_foundry_project_id
}
```
Expand All @@ -98,9 +117,43 @@ resource "azurerm_role_assignment" "function_ai_foundry_user" {

See complete infrastructure code: [`terraform/function.tf`](terraform/function.tf)

### Part 2: Connecting Function Layer to foundry_basic
### Part 2: Automatic Resource Discovery

Your function layer receives 5 inputs from foundry_basic (4 resource IDs + project name). See [`terraform/variables.tf`](terraform/variables.tf) for the complete interface.

Your function layer receives foundry_basic outputs as input variables. See [`terraform/variables.tf`](terraform/variables.tf) for the complete interface.
**How it works:**

```hcl
# 1. Parse resource IDs using the AzAPI provider function
# Signature: parse_resource_id(resource_type, resource_id)
locals {
ai_foundry_parsed = provider::azapi::parse_resource_id("Microsoft.CognitiveServices/accounts", var.foundry_ai_foundry_id)
app_insights_parsed = provider::azapi::parse_resource_id("Microsoft.Insights/components", var.foundry_application_insights_id)
}

# 2. Extract components using dot notation
locals {
foundry_resource_group_name = local.ai_foundry_parsed.resource_group_name
ai_foundry_name = local.ai_foundry_parsed.name

app_insights_resource_group = local.app_insights_parsed.resource_group_name
app_insights_name = local.app_insights_parsed.name

# Use project name directly from foundry_basic output
ai_foundry_project_name = var.foundry_ai_foundry_project_name
}

# 3. Use parsed names in data sources to discover resources
data "azurerm_cognitive_account" "ai_foundry" {
name = local.ai_foundry_name
resource_group_name = local.foundry_resource_group_name
}

data "azurerm_application_insights" "this" {
name = local.app_insights_name
resource_group_name = local.app_insights_resource_group
}
```

See complete setup: [`terraform/main.tf`](terraform/main.tf)

Expand All @@ -115,7 +168,7 @@ from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient

def get_project_client() -> AIProjectClient:
credential = DefaultAzureCredential() # Automatically uses managed identity!
credential = DefaultAzureCredential() # Automatically uses managed identity
endpoint = os.getenv("AI_FOUNDRY_ENDPOINT") # From terraform config

# Transform to AI Foundry project endpoint format
Expand Down Expand Up @@ -179,7 +232,7 @@ This validates the entire integration: agent creation, conversation, code interp
**Connection flow:**

1. User sends HTTPS request to function endpoint
1. Function runtime loads environment variables (AI Foundry endpoint from terraform)
1. Function runtime loads environment variables (AI Foundry endpoint discovered from data source)
1. DefaultAzureCredential requests token using managed identity
1. Azure AD validates the identity and returns access token
1. AI Projects SDK calls AI Foundry with the token
Expand All @@ -205,17 +258,28 @@ cd scripts
./configure-local-settings.sh
```

The script uses terraform outputs to extract all needed configuration values.

See local setup details: [`scripts/configure-local-settings.sh`](scripts/configure-local-settings.sh)

## Key CAIRA Integration Patterns

### 1. Consuming foundry_basic Outputs
### 1. Configuration with Automatic Discovery

Your function layer receives 5 inputs from foundry_basic:

Your function layer receives everything it needs from foundry_basic as terraform variables:
- AI Foundry resource ID (contains resource group and account name)
- AI Foundry project resource ID
- AI Foundry project name (explicit from foundry_basic output)
- Application Insights resource ID
- Log Analytics workspace resource ID

- AI Foundry endpoint and project details
- Application Insights connection
- Log Analytics workspace for diagnostic logs
Everything else is automatically discovered:

- Resource group name β†’ Parsed from AI Foundry ID using azapi provider
- AI Foundry account name β†’ Parsed from AI Foundry ID using azapi provider
- AI Foundry endpoint β†’ Retrieved via data source
- Application Insights name β†’ Parsed from Application Insights ID using azapi provider

See the complete variable interface: [`terraform/variables.tf`](terraform/variables.tf)

Expand Down Expand Up @@ -341,14 +405,10 @@ terraform init
terraform apply

# Capture outputs for the function layer
RG_NAME=$(terraform output -raw resource_group_name)
AI_FOUNDRY_NAME=$(terraform output -raw ai_foundry_name)
AI_FOUNDRY_ENDPOINT=$(terraform output -raw ai_foundry_endpoint)
AI_FOUNDRY_ID=$(terraform output -raw ai_foundry_id)
AI_PROJECT_ID=$(terraform output -raw ai_foundry_project_id)
AI_PROJECT_NAME=$(terraform output -raw ai_foundry_project_name)
APPINSIGHTS_ID=$(terraform output -raw application_insights_id)
APPINSIGHTS_NAME=$(basename "$APPINSIGHTS_ID")
APP_INSIGHTS_ID=$(terraform output -raw application_insights_id)
LOG_WORKSPACE_ID=$(terraform output -raw log_analytics_workspace_id)
```

Expand All @@ -359,12 +419,10 @@ cd ../../guides/implement_ai_foundry_basic_with_azure_function_integration/terra

# Create terraform.tfvars with the captured values
cat > terraform.tfvars <<EOF
foundry_resource_group_name = "$RG_NAME"
foundry_ai_foundry_id = "$AI_FOUNDRY_ID"
foundry_ai_foundry_endpoint = "$AI_FOUNDRY_ENDPOINT"
foundry_ai_foundry_project_id = "$AI_PROJECT_ID"
foundry_ai_foundry_project_name = "$AI_PROJECT_NAME"
foundry_application_insights_name = "$APPINSIGHTS_NAME"
foundry_application_insights_id = "$APP_INSIGHTS_ID"
foundry_log_analytics_workspace_id = "$LOG_WORKSPACE_ID"
project_name = "ai-integration"
function_sku_size = "B1"
Expand All @@ -381,7 +439,7 @@ terraform apply
- RBAC role granting AI Foundry access
- Storage account for function runtime
- Application Insights integration
- All configuration wired automatically
- All configuration wired automatically through resource discovery

### Step 3: Deploy Function Code

Expand Down Expand Up @@ -545,6 +603,13 @@ az functionapp identity show --name $FUNCTION_APP_NAME --resource-group $RESOURC

# Check deployed functions
az functionapp function list --name $FUNCTION_APP_NAME --resource-group $RESOURCE_GROUP --output table

# Verify parsed values from terraform
cd terraform
terraform console
> local.foundry_resource_group_name
> local.ai_foundry_name
> local.ai_foundry_project_name
```

## Monitoring
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# ---------------------------------------------------------------------

# configure-local-settings.sh - Configure local settings for AI Foundry integration
# Gets all values from the functions layer deployment

set -e

Expand All @@ -24,28 +23,16 @@ if [ ! -f "terraform.tfstate" ]; then
exit 1
fi

echo "Fetching configuration from functions layer..."
echo "Extracting configuration from terraform outputs..."

# Get values from terraform outputs
# Get all values from terraform outputs
FUNCTION_APP_NAME=$(terraform output -raw function_app_name)
FUNCTION_APP_URL=$(terraform output -raw function_app_url)

# Get the foundry values from terraform state (these are from data sources)
AI_FOUNDRY_NAME=$(terraform state show data.azurerm_cognitive_account.ai_foundry | grep "^\s*name\s*=" | head -1 | awk -F'"' '{print $2}')
RESOURCE_GROUP=$(terraform state show data.azurerm_resource_group.this | grep "^\s*name\s*=" | head -1 | awk -F'"' '{print $2}')

# Get project name and ID from terraform variables/state
AI_FOUNDRY_PROJECT_NAME=$(terraform state show var.foundry_ai_foundry_project_name | grep "^\s*value\s*=" | awk -F'"' '{print $2}')
AI_FOUNDRY_PROJECT_ID=$(terraform state show var.foundry_ai_foundry_project_id | grep "^\s*value\s*=" | awk -F'"' '{print $2}')

# Get AI Foundry endpoint using Azure CLI
AI_FOUNDRY_ENDPOINT=$(az cognitiveservices account show \
--name "$AI_FOUNDRY_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "properties.endpoint" -o tsv)

# Get subscription ID from Azure CLI
AZURE_SUBSCRIPTION_ID=$(az account show --query id -o tsv)
RESOURCE_GROUP=$(terraform output -raw foundry_resource_group_name)
AI_FOUNDRY_ENDPOINT=$(terraform output -raw ai_foundry_endpoint)
AI_FOUNDRY_PROJECT_NAME=$(terraform output -raw ai_foundry_project_name)
AI_FOUNDRY_PROJECT_ID=$(terraform output -raw ai_foundry_project_id)
AZURE_SUBSCRIPTION_ID=$(terraform output -raw subscription_id)

# Navigate to function-app directory
cd ../function-app
Expand Down Expand Up @@ -81,10 +68,10 @@ echo ""
echo "Configuration Summary:"
echo "----------------------"
echo "Resource Group: $RESOURCE_GROUP"
echo "AI Foundry Name: $AI_FOUNDRY_NAME"
echo "AI Foundry Endpoint: $AI_FOUNDRY_ENDPOINT"
echo "AI Foundry Project: $AI_FOUNDRY_PROJECT_NAME"
echo "AI Foundry Project ID: $AI_FOUNDRY_PROJECT_ID"
echo "Subscription ID: $AZURE_SUBSCRIPTION_ID"
echo "Function App Name: $FUNCTION_APP_NAME"
echo "Function App URL: $FUNCTION_APP_URL"
echo ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# Storage Account for Function App
resource "azurerm_storage_account" "function" {
name = replace(module.naming.storage_account.name_unique, "-", "")
resource_group_name = local.resource_group_name
resource_group_name = local.function_resource_group_name
location = local.location
account_tier = "Standard"
account_replication_type = "LRS"
Expand Down Expand Up @@ -39,7 +39,7 @@ resource "azurerm_storage_account" "function" {
# App Service Plan for Function App
resource "azurerm_service_plan" "function" {
name = module.naming.app_service_plan.name_unique
resource_group_name = local.resource_group_name
resource_group_name = local.function_resource_group_name
location = local.location
os_type = "Linux"
sku_name = var.function_sku_size
Expand All @@ -52,7 +52,7 @@ resource "azurerm_service_plan" "function" {
# Linux Function App with Managed Identity
resource "azurerm_linux_function_app" "main" {
name = local.function_app_name
resource_group_name = local.resource_group_name
resource_group_name = local.function_resource_group_name
location = local.location
service_plan_id = azurerm_service_plan.function.id

Expand Down Expand Up @@ -86,8 +86,8 @@ resource "azurerm_linux_function_app" "main" {
# Application settings
app_settings = {
"FUNCTIONS_WORKER_RUNTIME" = "python"
"AI_FOUNDRY_ENDPOINT" = var.foundry_ai_foundry_endpoint
"AI_FOUNDRY_PROJECT_NAME" = var.foundry_ai_foundry_project_name
"AI_FOUNDRY_ENDPOINT" = local.ai_foundry_endpoint
"AI_FOUNDRY_PROJECT_NAME" = local.ai_foundry_project_name
"AI_FOUNDRY_PROJECT_ID" = var.foundry_ai_foundry_project_id
"APPLICATIONINSIGHTS_CONNECTION_STRING" = data.azurerm_application_insights.this.connection_string
"AzureWebJobsStorage__accountName" = azurerm_storage_account.function.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,63 @@
# Prerequisites: any accessible foundry instance
############################################################

# Data sources for existing foundry_basic resources
locals {
# Parse standard Azure resource IDs using azapi provider function
# Signature: parse_resource_id(resource_type, resource_id)
ai_foundry_parsed = provider::azapi::parse_resource_id("Microsoft.CognitiveServices/accounts", var.foundry_ai_foundry_id)
app_insights_parsed = provider::azapi::parse_resource_id("Microsoft.Insights/components", var.foundry_application_insights_id)

# Extract components from parsed resource IDs
foundry_resource_group_name = local.ai_foundry_parsed.resource_group_name
ai_foundry_name = local.ai_foundry_parsed.name

app_insights_resource_group = local.app_insights_parsed.resource_group_name
app_insights_name = local.app_insights_parsed.name

ai_foundry_project_name = var.foundry_ai_foundry_project_name

# Validation using the parsed objects
is_valid_ai_foundry_id = (
can(local.ai_foundry_parsed.resource_group_name) &&
can(local.ai_foundry_parsed.name)
)

is_valid_app_insights_id = (
can(local.app_insights_parsed.resource_group_name) &&
can(local.app_insights_parsed.name)
)
}

# Validation check
resource "terraform_data" "validate_inputs" {
lifecycle {
precondition {
condition = local.is_valid_ai_foundry_id
error_message = "foundry_ai_foundry_id must be a valid Azure Cognitive Services resource ID with format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{name}"
}

precondition {
condition = local.is_valid_app_insights_id
error_message = "foundry_application_insights_id must be a valid Application Insights resource ID with format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Insights/components/{name}"
}
}
}

# Data source for existing foundry_basic resource group
data "azurerm_resource_group" "this" {
name = var.foundry_resource_group_name
name = local.foundry_resource_group_name
}

# Data source to reference the existing AI Foundry account and get endpoint
data "azurerm_cognitive_account" "ai_foundry" {
name = local.ai_foundry_name
resource_group_name = local.foundry_resource_group_name
}

# Data source to reference the existing Application Insights
data "azurerm_application_insights" "this" {
name = var.foundry_application_insights_name
resource_group_name = var.foundry_resource_group_name
name = local.app_insights_name
resource_group_name = local.app_insights_resource_group
}

# Naming module
Expand All @@ -42,15 +90,18 @@ resource "azurerm_resource_group" "function" {
Parent = data.azurerm_resource_group.this.name
}
)

depends_on = [terraform_data.validate_inputs]
}

# Local values
locals {
base_name = var.project_name

# Use the separate resource group
resource_group_name = azurerm_resource_group.function.name
location = azurerm_resource_group.function.location
# Use the separate resource group for functions
function_resource_group_name = azurerm_resource_group.function.name
location = azurerm_resource_group.function.location

function_app_name = module.naming.function_app.name_unique
function_app_name = module.naming.function_app.name_unique
ai_foundry_endpoint = data.azurerm_cognitive_account.ai_foundry.endpoint
}
Loading
Loading