Skip to content

Commit 9ca69e4

Browse files
BYO Storage for Speech and Language in Foundry resources (#337)
* Included BYOS for Speech and Language BICEP * allowProjectManagement changed to true * updated bicep template to include storage account check * allowing for template to take in storage accounts from another subscriptions, but not create one in another sub. * updated README.md * updated README.md for final check
1 parent aaed501 commit 9ca69e4

File tree

4 files changed

+218
-0
lines changed

4 files changed

+218
-0
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Bring Your Own Azure Storage for Speech and Language capabilities (Preview)
2+
3+
Use this template to associate your Azure Storage account to a new Foundry resource. This template allows you to bring an existing Azure Storage account as the storage for your Speech and Language usecases. Learn more about this feature's restrictions via our public documentation.
4+
5+
## Advanced implementation details
6+
### How is this setup different from an Azure Storage connection in Foundry?
7+
This template does not use the Foundry connections API. The [Foundry resource connections API](https://learn.microsoft.com/en-us/rest/api/aifoundry/accountmanagement/account-connections?view=rest-aifoundry-accountmanagement-2025-06-01) and [Foundry project connections API](https://learn.microsoft.com/en-us/rest/api/aifoundry/aiprojects/connections?view=rest-aifoundry-aiprojects-v1) use the Foundry endpoint to store authentication details like API keys and target. The Azure Storage association to a Foundry resource does not use that API. Instead, there is a field under `properties` in the Foundry resource creation step, that sets the Azure Storage resource ID. This field allows for backwards compatibility with how previous Speech Services and Language kinds do.
8+
9+
### What authentication methods are supported?
10+
This association does not support API key authentication, only RBAC authentication.
11+
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Run command examples:
2+
// For existing storage account: az deployment group create --name AzDeploy --resource-group {RESOURCE_GROUP} --template-file main.bicep --parameters aiFoundryName={AI_FOUNDRY_NAME} userOwnedStorageResourceId={STORAGE_ACCOUNT_RESOURCE_ID} location={LOCATION}
3+
// For new storage account: az deployment group create --name AzDeploy --resource-group {RESOURCE_GROUP} --template-file main.bicep --parameters aiFoundryName={AI_FOUNDRY_NAME} location={LOCATION}
4+
5+
@description('Name of the Azure AI Foundry account')
6+
param aiFoundryName string
7+
8+
@description('Location for the resource')
9+
param location string = resourceGroup().location
10+
11+
// Storage Account Parameters
12+
@description('Azure storage resource ID (leave empty to create a new storage account)')
13+
param userOwnedStorageResourceId string = ''
14+
15+
@description('Name of the Storage Account derived from the resource ID or provided for new account')
16+
param storageAccountName string = userOwnedStorageResourceId != '' ? last(split(userOwnedStorageResourceId, '/')) : 'st${uniqueString(subscription().subscriptionId, resourceGroup().name)}aaviles' // no '-' allowed
17+
18+
@description('Resource group name of the Storage Account derived from the resource ID or same as AI Foundry for new accounts')
19+
param storageResourceGroupName string = userOwnedStorageResourceId != '' ? split(userOwnedStorageResourceId, '/')[4] : resourceGroup().name
20+
21+
// Derived values for cross-subscription support
22+
var targetStorageSubscriptionId = userOwnedStorageResourceId != '' ? split(userOwnedStorageResourceId, '/')[2] : subscription().subscriptionId
23+
// --------------------------------
24+
25+
// ====================================================================
26+
// DEPLOYMENT STEPS OVERVIEW:
27+
// 1. Storage Account Configuration (existing vs new)
28+
// 2. Create Storage Account (if new - always in same RG as AI Foundry)
29+
// 3. Create AI Foundry with User Owned Storage
30+
// 4. Create Role Assignment for AI Foundry's managed identity on the Storage Account
31+
// ====================================================================
32+
33+
// ====================================================================
34+
// STEP 1: STORAGE ACCOUNT CONFIGURATION
35+
// ====================================================================
36+
@description('Automatically determined based on whether userOwnedStorageResourceId is provided')
37+
var useExistingStorageAccount bool = userOwnedStorageResourceId != ''
38+
39+
// ====================================================================
40+
// STEP 2: CREATE STORAGE ACCOUNT (IF NOT USING EXISTING ONE)
41+
// ====================================================================
42+
// New storage accounts are always created in the same resource group as AI Foundry
43+
// For cross-subscription existing storage, we use the target subscription for role assignment
44+
45+
// For cross-subscription storage account creation (not recommended, but supported)
46+
module storageAccountModule './modules/storageAccount.bicep' = if (!useExistingStorageAccount && targetStorageSubscriptionId != subscription().subscriptionId) {
47+
name: 'crossSubStorageAccount'
48+
scope: resourceGroup(targetStorageSubscriptionId, resourceGroup().name)
49+
params: {
50+
storageAccountName: storageAccountName
51+
location: location
52+
}
53+
}
54+
55+
// For same-subscription storage account creation (recommended)
56+
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = if (!useExistingStorageAccount && targetStorageSubscriptionId == subscription().subscriptionId) {
57+
name: storageAccountName
58+
location: location
59+
sku: {
60+
name: 'Standard_RAGRS'
61+
}
62+
kind: 'StorageV2'
63+
properties: {
64+
// For Azure AI Foundry BYOS, these settings are required
65+
allowBlobPublicAccess: false
66+
allowSharedKeyAccess: true // Must be true for AI Foundry to access
67+
minimumTlsVersion: 'TLS1_2'
68+
supportsHttpsTrafficOnly: true
69+
encryption: {
70+
services: {
71+
blob: {
72+
enabled: true
73+
}
74+
file: {
75+
enabled: true
76+
}
77+
}
78+
keySource: 'Microsoft.Storage'
79+
}
80+
// Required for AI Foundry BYOS
81+
allowCrossTenantReplication: false
82+
defaultToOAuthAuthentication: false
83+
}
84+
}
85+
86+
// ====================================================================
87+
// STEP 3: CREATE AI FOUNDRY WITH USER OWNED STORAGE
88+
// ====================================================================
89+
resource aiFoundry 'Microsoft.CognitiveServices/accounts@2025-07-01-preview' = {
90+
name: aiFoundryName
91+
location: location
92+
sku: {
93+
name: 'S0'
94+
}
95+
kind: 'AIServices'
96+
identity: {
97+
type: 'SystemAssigned'
98+
}
99+
properties: {
100+
allowProjectManagement: true
101+
publicNetworkAccess: 'Enabled' // or 'Disabled'
102+
disableLocalAuth: false
103+
customSubDomainName: toLower(replace(aiFoundryName, '_', '-'))
104+
userOwnedStorage: [{
105+
resourceId: useExistingStorageAccount ? userOwnedStorageResourceId : '/subscriptions/${targetStorageSubscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Storage/storageAccounts/${storageAccountName}'
106+
}]
107+
}
108+
}
109+
110+
// ====================================================================
111+
// STEP 4: CREATE ROLE ASSIGNMENT FOR AI FOUNDRY'S MANAGED IDENTITY
112+
// ====================================================================
113+
// Role assignment deployed to the correct subscription/resource group based on storage location
114+
module roleAssignmentModule './modules/roleAssignment.bicep' = {
115+
name: 'storageRoleAssignment'
116+
scope: resourceGroup(targetStorageSubscriptionId, storageResourceGroupName)
117+
params: {
118+
storageAccountName: storageAccountName
119+
principalId: aiFoundry.identity.principalId
120+
aiFoundryResourceId: aiFoundry.id
121+
}
122+
}
123+
124+
// ====================================================================
125+
// OUTPUTS
126+
// ====================================================================
127+
@description('The resource ID of the AI Foundry account')
128+
output aiFoundryId string = aiFoundry.id
129+
130+
@description('The name of the AI Foundry account')
131+
output aiFoundryName string = aiFoundry.name
132+
133+
@description('The resource ID of the storage account being used')
134+
output storageAccountId string = useExistingStorageAccount ? userOwnedStorageResourceId : '/subscriptions/${targetStorageSubscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Storage/storageAccounts/${storageAccountName}'
135+
136+
@description('The managed identity principal ID of the AI Foundry account')
137+
output aiFoundryPrincipalId string = aiFoundry.identity.principalId
138+
139+
@description('The subscription ID where the storage account is located')
140+
output storageSubscriptionId string = targetStorageSubscriptionId
141+
142+
@description('Status of role assignment')
143+
output roleAssignmentStatus string = 'Storage Blob Data Contributor role assigned automatically via module'
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@description('Name of the Storage Account')
2+
param storageAccountName string
3+
4+
@description('Principal ID of the AI Foundry managed identity')
5+
param principalId string
6+
7+
@description('AI Foundry resource ID for generating unique role assignment name')
8+
param aiFoundryResourceId string
9+
10+
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
11+
name: storageAccountName
12+
}
13+
14+
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
15+
name: guid(aiFoundryResourceId, 'Storage Blob Data Contributor')
16+
scope: storageAccount
17+
properties: {
18+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe') // Storage Blob Data Contributor
19+
principalId: principalId
20+
principalType: 'ServicePrincipal'
21+
}
22+
}
23+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
@description('Name of the Storage Account')
2+
param storageAccountName string
3+
4+
@description('Location for the storage account')
5+
param location string
6+
7+
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-01-01' = {
8+
name: storageAccountName
9+
location: location
10+
sku: {
11+
name: 'Standard_RAGRS'
12+
}
13+
kind: 'StorageV2'
14+
properties: {
15+
// For Azure AI Foundry BYOS, these settings are required
16+
allowBlobPublicAccess: false
17+
allowSharedKeyAccess: true // Must be true for AI Foundry to access
18+
minimumTlsVersion: 'TLS1_2'
19+
supportsHttpsTrafficOnly: true
20+
encryption: {
21+
services: {
22+
blob: {
23+
enabled: true
24+
}
25+
file: {
26+
enabled: true
27+
}
28+
}
29+
keySource: 'Microsoft.Storage'
30+
}
31+
// Required for AI Foundry BYOS
32+
allowCrossTenantReplication: false
33+
defaultToOAuthAuthentication: false
34+
}
35+
}
36+
37+
@description('Resource ID of the created storage account')
38+
output storageAccountId string = storageAccount.id
39+
40+
@description('Name of the created storage account')
41+
output storageAccountName string = storageAccount.name

0 commit comments

Comments
 (0)