Skip to content

Commit be56540

Browse files
Chen, VivienChen, Vivien
authored andcommitted
Merge branch 'Development' of https://github.com/vivche/simplechat-development into Development
2 parents 164dd80 + 4e4f105 commit be56540

2 files changed

Lines changed: 317 additions & 15 deletions

File tree

‎deployers/terraform/main.tf‎

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ locals {
172172
cosmos_db_name = "${var.param_base_name}-${var.param_environment}-cosmos"
173173
open_ai_name = "${var.param_base_name}-${var.param_environment}-oai"
174174
doc_intel_name = "${var.param_base_name}-${var.param_environment}-docintel"
175+
speech_service_name = "${var.param_base_name}-${var.param_environment}-speech"
175176
key_vault_name = "${var.param_base_name}-${var.param_environment}-kv"
176177
log_analytics_name = "${var.param_base_name}-${var.param_environment}-la"
177178
managed_identity_name = "${var.param_base_name}-${var.param_environment}-id"
@@ -625,13 +626,14 @@ resource "azurerm_cosmosdb_account" "cosmos" {
625626

626627
# --- Azure OpenAI Service (Cognitive Services) ---
627628
resource "azurerm_cognitive_account" "openai" {
628-
count = var.param_use_existing_openai_instance ? 0 : 1 # Only create if not using existing
629-
name = local.open_ai_name
630-
location = azurerm_resource_group.rg.location
631-
resource_group_name = azurerm_resource_group.rg.name
632-
kind = "OpenAI"
633-
sku_name = "S0" # Standard tier
634-
tags = local.common_tags
629+
count = var.param_use_existing_openai_instance ? 0 : 1 # Only create if not using existing
630+
name = local.open_ai_name
631+
location = azurerm_resource_group.rg.location
632+
resource_group_name = azurerm_resource_group.rg.name
633+
kind = "OpenAI"
634+
sku_name = "S0" # Standard tier
635+
custom_subdomain_name = local.open_ai_name # Required for managed identity authentication
636+
tags = local.common_tags
635637
}
636638

637639
# Data source for existing OpenAI instance
@@ -643,13 +645,24 @@ data "azurerm_cognitive_account" "existing_openai" {
643645

644646
# --- Document Intelligence Service (Cognitive Services) ---
645647
resource "azurerm_cognitive_account" "docintel" {
646-
name = local.doc_intel_name
647-
location = azurerm_resource_group.rg.location
648-
resource_group_name = azurerm_resource_group.rg.name
649-
kind = "FormRecognizer"
650-
sku_name = "S0" # Standard tier
651-
custom_subdomain_name = local.doc_intel_name # Maps to --custom-domain
652-
tags = local.common_tags
648+
name = local.doc_intel_name
649+
location = azurerm_resource_group.rg.location
650+
resource_group_name = azurerm_resource_group.rg.name
651+
kind = "FormRecognizer"
652+
sku_name = "S0" # Standard tier
653+
custom_subdomain_name = local.doc_intel_name # Required for managed identity authentication
654+
tags = local.common_tags
655+
}
656+
657+
# --- Speech Service (Cognitive Services) ---
658+
resource "azurerm_cognitive_account" "speech" {
659+
name = local.speech_service_name
660+
location = azurerm_resource_group.rg.location
661+
resource_group_name = azurerm_resource_group.rg.name
662+
kind = "SpeechServices"
663+
sku_name = "S0" # Standard tier
664+
custom_subdomain_name = local.speech_service_name # Required for managed identity authentication
665+
tags = local.common_tags
653666
}
654667

655668
# https://medium.com/expert-thinking/mastering-azure-search-with-terraform-a-how-to-guide-7edc3a6b1ee3
@@ -702,6 +715,20 @@ resource "azurerm_role_assignment" "managed_identity_storage_contributor" {
702715
principal_id = azurerm_user_assigned_identity.id.principal_id
703716
}
704717

718+
# Cognitive Services Speech User on Speech Service
719+
resource "azurerm_role_assignment" "managed_identity_speech_user" {
720+
scope = azurerm_cognitive_account.speech.id
721+
role_definition_name = "Cognitive Services Speech User"
722+
principal_id = azurerm_user_assigned_identity.id.principal_id
723+
}
724+
725+
# Cognitive Services Speech Contributor on Speech Service
726+
resource "azurerm_role_assignment" "managed_identity_speech_contributor" {
727+
scope = azurerm_cognitive_account.speech.id
728+
role_definition_name = "Cognitive Services Speech Contributor"
729+
principal_id = azurerm_user_assigned_identity.id.principal_id
730+
}
731+
705732
# App Registration Service Principal RBAC
706733
# Cognitive Services OpenAI Contributor on OpenAI
707734
resource "azurerm_role_assignment" "app_reg_sp_openai_contributor" {
@@ -732,13 +759,27 @@ resource "azurerm_role_assignment" "app_service_smi_storage_contributor" {
732759
principal_id = azurerm_linux_web_app.app.identity[0].principal_id
733760
}
734761

735-
# Storage Blob Data Contributor on Storage Account
762+
# AcrPull on Container Registry
736763
resource "azurerm_role_assignment" "acr_pull" {
737764
scope = data.azurerm_container_registry.acrregistry.id
738765
role_definition_name = "AcrPull"
739766
principal_id = azurerm_linux_web_app.app.identity[0].principal_id
740767
}
741768

769+
# Cognitive Services Speech User on Speech Service
770+
resource "azurerm_role_assignment" "app_service_smi_speech_user" {
771+
scope = azurerm_cognitive_account.speech.id
772+
role_definition_name = "Cognitive Services Speech User"
773+
principal_id = azurerm_linux_web_app.app.identity[0].principal_id
774+
}
775+
776+
# Cognitive Services Speech Contributor on Speech Service
777+
resource "azurerm_role_assignment" "app_service_smi_speech_contributor" {
778+
scope = azurerm_cognitive_account.speech.id
779+
role_definition_name = "Cognitive Services Speech Contributor"
780+
principal_id = azurerm_linux_web_app.app.identity[0].principal_id
781+
}
782+
742783

743784
##################################################
744785
#
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# Azure Speech Service with Managed Identity Setup
2+
3+
## Overview
4+
5+
This guide explains the critical difference between key-based and managed identity authentication when configuring Azure Speech Service, and the required steps to enable managed identity properly.
6+
7+
## Authentication Methods: Regional vs. Resource-Specific Endpoints
8+
9+
### Regional Endpoint (Shared Gateway)
10+
11+
**Endpoint format**: `https://<region>.api.cognitive.microsoft.com`
12+
- Example: `https://eastus2.api.cognitive.microsoft.com`
13+
- This is a **shared endpoint** for all Speech resources in that Azure region
14+
- Acts as a gateway that routes requests to individual Speech resources
15+
16+
### Resource-Specific Endpoint (Custom Subdomain)
17+
18+
**Endpoint format**: `https://<resource-name>.cognitiveservices.azure.com`
19+
- Example: `https://simplechat6-dev-speech.cognitiveservices.azure.com`
20+
- This is a **unique endpoint** dedicated to your specific Speech resource
21+
- Requires custom subdomain to be enabled on the resource
22+
23+
---
24+
25+
## Why Regional Endpoint Works with Key but NOT Managed Identity
26+
27+
### Key-Based Authentication ✅ Works with Regional Endpoint
28+
29+
When using subscription key authentication:
30+
31+
```http
32+
POST https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe
33+
Headers:
34+
Ocp-Apim-Subscription-Key: abc123def456...
35+
```
36+
37+
**Why it works:**
38+
1. The subscription key **directly identifies** your specific Speech resource
39+
2. The regional gateway uses the key to look up which resource it belongs to
40+
3. The request is automatically routed to your resource
41+
4. Authorization succeeds because the key proves ownership
42+
43+
### Managed Identity (AAD Token) ❌ Fails with Regional Endpoint
44+
45+
When using managed identity authentication:
46+
47+
```http
48+
POST https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe
49+
Headers:
50+
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
51+
```
52+
53+
**Why it fails (returns 400 BadRequest):**
54+
1. The Bearer token proves your App Service identity to Azure AD
55+
2. The token does NOT specify which Speech resource you want to access
56+
3. The regional gateway cannot determine:
57+
- Which specific Speech resource you're authorized for
58+
- Whether your managed identity has RBAC roles on that resource
59+
4. **Result**: The gateway rejects the request with 400 BadRequest
60+
61+
### Managed Identity ✅ Works with Resource-Specific Endpoint
62+
63+
When using managed identity with custom subdomain:
64+
65+
```http
66+
POST https://simplechat6-dev-speech.cognitiveservices.azure.com/speechtotext/transcriptions:transcribe
67+
Headers:
68+
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGc...
69+
```
70+
71+
**Why it works:**
72+
1. The hostname **itself identifies** your specific Speech resource
73+
2. Azure validates your managed identity Bearer token against that resource's RBAC
74+
3. If your App Service MI has `Cognitive Services Speech User` role → authorized
75+
4. The request proceeds to your dedicated Speech resource instance
76+
77+
---
78+
79+
## Required Setup for Managed Identity
80+
81+
### Prerequisites
82+
83+
1. **Azure Speech Service resource** created in your subscription
84+
2. **System-assigned or user-assigned managed identity** on your App Service
85+
3. **RBAC role assignments** on the Speech resource
86+
87+
### Step 1: Enable Custom Subdomain on Speech Resource
88+
89+
**Why needed**: By default, Speech resources use the regional endpoint and do NOT have custom subdomains. Managed identity requires the resource-specific endpoint.
90+
91+
**How to enable**:
92+
93+
```bash
94+
az cognitiveservices account update \
95+
--name <speech-resource-name> \
96+
--resource-group <resource-group-name> \
97+
--custom-domain <speech-resource-name>
98+
```
99+
100+
**Example**:
101+
102+
```bash
103+
az cognitiveservices account update \
104+
--name simplechat6-dev-speech \
105+
--resource-group sc-simplechat6-dev-rg \
106+
--custom-domain simplechat6-dev-speech
107+
```
108+
109+
**Important notes**:
110+
- Custom subdomain name must be **globally unique** across Azure
111+
- Usually use the same name as your resource: `<resource-name>`
112+
- **One-way operation**: Cannot be disabled once enabled
113+
- After enabling, the resource's endpoint property changes from regional to resource-specific
114+
115+
**Verify custom subdomain is enabled**:
116+
117+
```bash
118+
az cognitiveservices account show \
119+
--name <speech-resource-name> \
120+
--resource-group <resource-group-name> \
121+
--query "{customSubDomainName:properties.customSubDomainName, endpoint:properties.endpoint}"
122+
```
123+
124+
Expected output:
125+
```json
126+
{
127+
"customSubDomainName": "simplechat6-dev-speech",
128+
"endpoint": "https://simplechat6-dev-speech.cognitiveservices.azure.com/"
129+
}
130+
```
131+
132+
### Step 2: Assign RBAC Roles to Managed Identity
133+
134+
Grant your App Service managed identity the necessary roles on the Speech resource:
135+
136+
```bash
137+
# Get the Speech resource ID
138+
SPEECH_RESOURCE_ID=$(az cognitiveservices account show \
139+
--name <speech-resource-name> \
140+
--resource-group <resource-group-name> \
141+
--query id -o tsv)
142+
143+
# Get the App Service managed identity principal ID
144+
MI_PRINCIPAL_ID=$(az webapp identity show \
145+
--name <app-service-name> \
146+
--resource-group <resource-group-name> \
147+
--query principalId -o tsv)
148+
149+
# Assign Cognitive Services Speech User role (data-plane read access)
150+
az role assignment create \
151+
--assignee $MI_PRINCIPAL_ID \
152+
--role "Cognitive Services Speech User" \
153+
--scope $SPEECH_RESOURCE_ID
154+
155+
# Assign Cognitive Services Speech Contributor role (if needed for write operations)
156+
az role assignment create \
157+
--assignee $MI_PRINCIPAL_ID \
158+
--role "Cognitive Services Speech Contributor" \
159+
--scope $SPEECH_RESOURCE_ID
160+
```
161+
162+
**Verify role assignments**:
163+
164+
```bash
165+
az role assignment list \
166+
--assignee $MI_PRINCIPAL_ID \
167+
--scope $SPEECH_RESOURCE_ID \
168+
-o table
169+
```
170+
171+
### Step 3: Configure Admin Settings
172+
173+
In the Admin Settings → Search & Extract → Multimedia Support section:
174+
175+
| Setting | Value | Example |
176+
|---------|-------|---------|
177+
| **Enable Audio File Support** | ✅ Checked | |
178+
| **Speech Service Endpoint** | Resource-specific endpoint (with custom subdomain) | `https://simplechat6-dev-speech.cognitiveservices.azure.com` |
179+
| **Speech Service Location** | Azure region | `eastus2` |
180+
| **Speech Service Locale** | Language locale for transcription | `en-US` |
181+
| **Authentication Type** | Managed Identity | |
182+
| **Speech Service Key** | (Leave empty when using MI) | |
183+
184+
**Critical**:
185+
- Endpoint must be the resource-specific URL (custom subdomain)
186+
- Do NOT use the regional endpoint for managed identity
187+
- Remove trailing slash from endpoint: ✅ `https://..azure.com` ❌ `https://..azure.com/`
188+
189+
### Step 4: Test Audio Upload
190+
191+
1. Upload a short WAV or MP3 file
192+
2. Monitor application logs for transcription progress
193+
3. Expected log output:
194+
```
195+
File size: 1677804 bytes
196+
Produced 1 WAV chunks: ['/tmp/tmp_chunk_000.wav']
197+
[Debug] Transcribing WAV chunk: /tmp/tmp_chunk_000.wav
198+
[Debug] Speech config obtained successfully
199+
[Debug] Received 5 phrases
200+
Creating 3 transcript pages
201+
```
202+
203+
---
204+
205+
## Troubleshooting
206+
207+
### Error: NameResolutionError - Failed to resolve hostname
208+
209+
**Symptom**: `Failed to resolve 'simplechat6-dev-speech.cognitiveservices.azure.com'`
210+
211+
**Cause**: Custom subdomain not enabled on Speech resource
212+
213+
**Solution**: Enable custom subdomain using Step 1 above
214+
215+
### Error: 400 BadRequest when using MI with regional endpoint
216+
217+
**Symptom**: `400 Client Error: BadRequest for url: https://eastus2.api.cognitive.microsoft.com/speechtotext/transcriptions:transcribe`
218+
219+
**Cause**: Managed identity requires resource-specific endpoint, not regional
220+
221+
**Solution**: Update Admin Settings endpoint to use `https://<resource-name>.cognitiveservices.azure.com`
222+
223+
### Error: 401 Authentication error with MI
224+
225+
**Symptom**: `WebSocket upgrade failed: Authentication error (401)`
226+
227+
**Cause**: Missing RBAC role assignments
228+
229+
**Solution**: Assign required roles using Step 2 above
230+
231+
### Key auth works but MI fails
232+
233+
**Diagnosis checklist**:
234+
- [ ] Custom subdomain enabled on Speech resource?
235+
- [ ] Admin Settings endpoint is resource-specific (not regional)?
236+
- [ ] Managed identity has RBAC roles on Speech resource?
237+
- [ ] Authentication Type set to "Managed Identity" in Admin Settings?
238+
239+
---
240+
241+
## Summary
242+
243+
| Authentication Method | Endpoint Type | Example | Works? |
244+
|----------------------|---------------|---------|--------|
245+
| **Key** | Regional | `https://eastus2.api.cognitive.microsoft.com` | ✅ Yes |
246+
| **Key** | Resource-specific | `https://simplechat6-dev-speech.cognitiveservices.azure.com` | ✅ Yes |
247+
| **Managed Identity** | Regional | `https://eastus2.api.cognitive.microsoft.com` | ❌ No (400 BadRequest) |
248+
| **Managed Identity** | Resource-specific | `https://simplechat6-dev-speech.cognitiveservices.azure.com` | ✅ Yes (with custom subdomain) |
249+
250+
**Key takeaway**: Managed identity for Azure Cognitive Services data-plane operations requires:
251+
1. Custom subdomain enabled on the resource
252+
2. Resource-specific endpoint configured in your application
253+
3. RBAC roles assigned to the managed identity at the resource scope
254+
255+
---
256+
257+
## References
258+
259+
- [Azure Cognitive Services custom subdomain documentation](https://learn.microsoft.com/azure/cognitive-services/cognitive-services-custom-subdomains)
260+
- [Authenticate with Azure AD using managed identity](https://learn.microsoft.com/azure/cognitive-services/authentication?tabs=powershell#authenticate-with-azure-active-directory)
261+
- [Azure Speech Service authentication](https://learn.microsoft.com/azure/ai-services/speech-service/rest-speech-to-text-short)

0 commit comments

Comments
 (0)