Skip to content

Commit 3dd7f34

Browse files
Copiloteerhardt
andauthored
Add IAzurePrivateEndpointTarget support to AzureOpenAIResource and FoundryResource (#15945)
* Initial plan * Add IAzurePrivateEndpointTarget support to AzureOpenAIResource and FoundryResource Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/c060fb13-56b1-4905-9cbc-b78a181f5062 Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> * Replace interface-check tests with bicep-baseline tests for private endpoints Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/85ce9899-08f7-411c-b83d-e162ddb09217 Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> * Disable PublicNetworkAccess when private endpoints are used. * Move Id property back to original position in AzureOpenAIResource and FoundryResource Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/b73a625e-b303-4af6-9099-032be15a9605 Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> * Revert AzurePrivateEndpointExtensionsTests.cs to original state Agent-Logs-Url: https://github.com/microsoft/aspire/sessions/4d004e72-6da4-4c52-be06-fbb0d30d4bda Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eerhardt <8291187+eerhardt@users.noreply.github.com> Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
1 parent db444f8 commit 3dd7f34

File tree

7 files changed

+129
-4
lines changed

7 files changed

+129
-4
lines changed

src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
5+
46
using Aspire.Hosting.ApplicationModel;
57
using Aspire.Hosting.Azure;
68
using Aspire.Hosting.Azure.CognitiveServices;
@@ -38,6 +40,11 @@ public static IResourceBuilder<AzureOpenAIResource> AddAzureOpenAI(this IDistrib
3840

3941
var configureInfrastructure = (AzureResourceInfrastructure infrastructure) =>
4042
{
43+
var azureResource = (AzureOpenAIResource)infrastructure.AspireResource;
44+
45+
// Check if this Azure OpenAI has a private endpoint (via annotation)
46+
var hasPrivateEndpoint = azureResource.HasAnnotationOfType<PrivateEndpointTargetAnnotation>();
47+
4148
var cogServicesAccount = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infrastructure,
4249
(identifier, name) =>
4350
{
@@ -55,7 +62,9 @@ public static IResourceBuilder<AzureOpenAIResource> AddAzureOpenAI(this IDistrib
5562
Properties = new CognitiveServicesAccountProperties()
5663
{
5764
CustomSubDomainName = ToLower(Take(Concat(infrastructure.AspireResource.Name, GetUniqueString(GetResourceGroup().Id)), 24)),
58-
PublicNetworkAccess = ServiceAccountPublicNetworkAccess.Enabled,
65+
PublicNetworkAccess = hasPrivateEndpoint
66+
? ServiceAccountPublicNetworkAccess.Disabled
67+
: ServiceAccountPublicNetworkAccess.Enabled,
5968
// Disable local auth for AOAI since managed identity is used
6069
DisableLocalAuth = true
6170
},

src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIResource.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Aspire.Hosting.ApplicationModel;
1717
[AspireExport]
1818
public class AzureOpenAIResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
1919
: AzureProvisioningResource(name, configureInfrastructure),
20-
IResourceWithConnectionString, IAzureNspAssociationTarget
20+
IResourceWithConnectionString, IAzurePrivateEndpointTarget, IAzureNspAssociationTarget
2121
{
2222
[Obsolete("Use AzureOpenAIDeploymentResource instead.")]
2323
private readonly List<AzureOpenAIDeployment> _deployments = [];
@@ -111,6 +111,10 @@ public override ProvisionableResource AddAsExistingResource(AzureResourceInfrast
111111
return account;
112112
}
113113

114+
IEnumerable<string> IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["account"];
115+
116+
string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.openai.azure.com";
117+
114118
IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionString.GetConnectionProperties()
115119
{
116120
yield return new("Uri", UriExpression);

src/Aspire.Hosting.Foundry/FoundryExtensions.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
5+
46
using Aspire.Hosting.ApplicationModel;
57
using Aspire.Hosting.Azure;
68
using Aspire.Hosting.Foundry;
@@ -408,6 +410,11 @@ await rns.PublishUpdateAsync(deployment, state => state with
408410

409411
private static void ConfigureInfrastructure(AzureResourceInfrastructure infrastructure)
410412
{
413+
var azureResource = (FoundryResource)infrastructure.AspireResource;
414+
415+
// Check if this Foundry resource has a private endpoint (via annotation)
416+
var hasPrivateEndpoint = azureResource.HasAnnotationOfType<PrivateEndpointTargetAnnotation>();
417+
411418
var cogServicesAccount = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infrastructure,
412419
(identifier, name) =>
413420
{
@@ -427,7 +434,9 @@ private static void ConfigureInfrastructure(AzureResourceInfrastructure infrastr
427434
// Until this bug is fixed, CustomSubDomainName must be set to the
428435
// account's name: https://msdata.visualstudio.com/Vienna/_workitems/edit/4866592
429436
CustomSubDomainName = ToLower(Take(Concat(infrastructure.AspireResource.Name, GetUniqueString(GetResourceGroup().Id)), 24)),
430-
PublicNetworkAccess = ServiceAccountPublicNetworkAccess.Enabled,
437+
PublicNetworkAccess = hasPrivateEndpoint
438+
? ServiceAccountPublicNetworkAccess.Disabled
439+
: ServiceAccountPublicNetworkAccess.Enabled,
431440
DisableLocalAuth = true,
432441
AllowProjectManagement = true
433442
},

src/Aspire.Hosting.Foundry/FoundryResource.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ namespace Aspire.Hosting.Foundry;
1717
/// <param name="configureInfrastructure">Configures the underlying Azure resource using Azure.Provisioning.</param>
1818
[AspireExport]
1919
public class FoundryResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
20-
AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithConnectionString, IAzureNspAssociationTarget
20+
AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithConnectionString, IAzurePrivateEndpointTarget, IAzureNspAssociationTarget
2121
{
2222
internal Uri? EmulatorServiceUri { get; set; }
2323

@@ -132,6 +132,10 @@ IEnumerable<KeyValuePair<string, ReferenceExpression>> IResourceWithConnectionSt
132132
yield return new("Key", ReferenceExpression.Create($"{ApiKey}"));
133133
}
134134
}
135+
136+
IEnumerable<string> IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["account"];
137+
138+
string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.cognitiveservices.azure.com";
135139
}
136140

137141
/// <summary>

tests/Aspire.Hosting.Azure.Tests/AzurePrivateEndpointLockdownTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,4 +184,36 @@ public async Task AddAzureWebPubSub_WithPrivateEndpoint_GeneratesCorrectBicep()
184184

185185
await Verify(manifest.BicepText, extension: "bicep");
186186
}
187+
188+
[Fact]
189+
public async Task AddAzureOpenAI_WithPrivateEndpoint_GeneratesCorrectBicep()
190+
{
191+
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
192+
193+
var vnet = builder.AddAzureVirtualNetwork("myvnet");
194+
var subnet = vnet.AddSubnet("pesubnet", "10.0.1.0/24");
195+
var openai = builder.AddAzureOpenAI("openai");
196+
197+
subnet.AddPrivateEndpoint(openai);
198+
199+
var manifest = await AzureManifestUtils.GetManifestWithBicep(openai.Resource);
200+
201+
await Verify(manifest.BicepText, extension: "bicep");
202+
}
203+
204+
[Fact]
205+
public async Task AddFoundry_WithPrivateEndpoint_GeneratesCorrectBicep()
206+
{
207+
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
208+
209+
var vnet = builder.AddAzureVirtualNetwork("myvnet");
210+
var subnet = vnet.AddSubnet("pesubnet", "10.0.1.0/24");
211+
var foundry = builder.AddFoundry("foundry");
212+
213+
subnet.AddPrivateEndpoint(foundry);
214+
215+
var manifest = await AzureManifestUtils.GetManifestWithBicep(foundry.Resource);
216+
217+
await Verify(manifest.BicepText, extension: "bicep");
218+
}
187219
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
@description('The location for the resource(s) to be deployed.')
2+
param location string = resourceGroup().location
3+
4+
resource openai 'Microsoft.CognitiveServices/accounts@2025-09-01' = {
5+
name: take('openai-${uniqueString(resourceGroup().id)}', 64)
6+
location: location
7+
kind: 'OpenAI'
8+
properties: {
9+
customSubDomainName: toLower(take(concat('openai', uniqueString(resourceGroup().id)), 24))
10+
publicNetworkAccess: 'Disabled'
11+
disableLocalAuth: true
12+
}
13+
sku: {
14+
name: 'S0'
15+
}
16+
tags: {
17+
'aspire-resource-name': 'openai'
18+
}
19+
}
20+
21+
output connectionString string = 'Endpoint=${openai.properties.endpoint}'
22+
23+
output endpoint string = openai.properties.endpoint
24+
25+
output name string = openai.name
26+
27+
output id string = openai.id
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
@description('The location for the resource(s) to be deployed.')
2+
param location string = resourceGroup().location
3+
4+
resource foundry 'Microsoft.CognitiveServices/accounts@2025-09-01' = {
5+
name: take('foundry-${uniqueString(resourceGroup().id)}', 64)
6+
location: location
7+
identity: {
8+
type: 'SystemAssigned'
9+
}
10+
kind: 'AIServices'
11+
properties: {
12+
customSubDomainName: toLower(take(concat('foundry', uniqueString(resourceGroup().id)), 24))
13+
publicNetworkAccess: 'Disabled'
14+
disableLocalAuth: true
15+
allowProjectManagement: true
16+
}
17+
sku: {
18+
name: 'S0'
19+
}
20+
tags: {
21+
'aspire-resource-name': 'foundry'
22+
}
23+
}
24+
25+
resource foundry_caphost 'Microsoft.CognitiveServices/accounts/capabilityHosts@2025-10-01-preview' = {
26+
name: 'foundry-caphost'
27+
properties: {
28+
capabilityHostKind: 'Agents'
29+
enablePublicHostingEnvironment: true
30+
}
31+
parent: foundry
32+
}
33+
34+
output aiFoundryApiEndpoint string = foundry.properties.endpoints['AI Foundry API']
35+
36+
output endpoint string = foundry.properties.endpoint
37+
38+
output name string = foundry.name
39+
40+
output id string = foundry.id

0 commit comments

Comments
 (0)