Skip to content

Commit a0363ff

Browse files
authored
Implement AppServiceFeature (#47269)
1 parent a24e586 commit a0363ff

9 files changed

Lines changed: 151 additions & 31 deletions

File tree

eng/Packages.Data.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
<PackageReference Update="Azure.Storage.Blobs" Version="12.21.1" />
129129
<PackageReference Update="Azure.Storage.Queues" Version="12.19.1" />
130130
<PackageReference Update="Azure.Storage.Files.Shares" Version="12.19.1" />
131-
<PackageReference Update="Azure.AI.Inference" Version="1.0.0-beta.2" />
131+
<PackageReference Update="Azure.AI.Inference" Version="1.0.0-beta.2" />
132132
<PackageReference Update="Azure.AI.OpenAI" Version="2.0.0" />
133133
<PackageReference Update="Azure.ResourceManager" Version="1.13.0" />
134134
<PackageReference Update="Azure.ResourceManager.AppConfiguration" Version="1.3.2" />
@@ -158,6 +158,7 @@
158158
<PackageReference Update="Azure.Provisioning.KeyVault" Version="1.0.0" />
159159
<PackageReference Update="Azure.Provisioning.ServiceBus" Version="1.0.0" />
160160
<PackageReference Update="Azure.Provisioning.Storage" Version="1.0.0" />
161+
<PackageReference Update="Azure.Provisioning.AppService" Version="1.0.0" />
161162
<PackageReference Update="Microsoft.Bcl.Numerics" Version="8.0.0" />
162163

163164
<!-- Other approved packages -->

sdk/cloudmachine/Azure.CloudMachine/src/CloudMachineWorkspace.cs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,7 @@ namespace Azure.CloudMachine;
1818
/// </summary>
1919
public class CloudMachineWorkspace : ClientWorkspace
2020
{
21-
private TokenCredential Credential { get; } = new ChainedTokenCredential(
22-
new AzureCliCredential(),
23-
new AzureDeveloperCliCredential()
24-
);
21+
private TokenCredential Credential { get; }
2522

2623
/// <summary>
2724
/// The cloud machine ID.
@@ -42,20 +39,21 @@ public CloudMachineWorkspace(TokenCredential credential = default, IConfiguratio
4239
{
4340
Credential = credential;
4441
}
45-
46-
string cmid;
47-
if (configuration == default)
48-
{
49-
cmid = ReadOrCreateCmid();
50-
}
5142
else
5243
{
53-
cmid = configuration["CloudMachine:ID"];
54-
if (cmid == null)
55-
throw new Exception("CloudMachine:ID configuration value missing");
44+
// This environment variable is set by the CloudMachine App Service feature during provisioning.
45+
Credential = Environment.GetEnvironmentVariable("CLOUDMACHINE_MANAGED_IDENTITY_CLIENT_ID") switch
46+
{
47+
string clientId when !string.IsNullOrEmpty(clientId) => new ManagedIdentityCredential(clientId),
48+
_ => new ChainedTokenCredential(new AzureCliCredential(), new AzureDeveloperCliCredential())
49+
};
5650
}
5751

58-
Id = cmid!;
52+
Id = configuration switch
53+
{
54+
null => ReadOrCreateCmid(),
55+
_ => configuration["CloudMachine:ID"] ?? throw new Exception("CloudMachine:ID configuration value missing")
56+
};
5957
}
6058

6159
/// <summary>
@@ -69,7 +67,8 @@ public CloudMachineWorkspace(TokenCredential credential = default, IConfiguratio
6967
public override ClientConnectionOptions GetConnectionOptions(Type clientType, string instanceId)
7068
{
7169
string clientId = clientType.FullName;
72-
if (instanceId != null && instanceId.StartsWith("$")) clientId = $"{clientType.FullName}{instanceId}";
70+
if (instanceId != null && instanceId.StartsWith("$"))
71+
clientId = $"{clientType.FullName}{instanceId}";
7372

7473
switch (clientId)
7574
{
@@ -78,13 +77,13 @@ public override ClientConnectionOptions GetConnectionOptions(Type clientType, st
7877
case "Azure.Messaging.ServiceBus.ServiceBusClient":
7978
return new ClientConnectionOptions(new($"https://{Id}.servicebus.windows.net"), Credential);
8079
case "Azure.Messaging.ServiceBus.ServiceBusSender":
81-
return new ClientConnectionOptions(instanceId?? "cm_servicebus_default_topic");
80+
return new ClientConnectionOptions(instanceId ?? "cm_servicebus_default_topic");
8281
case "Azure.Messaging.ServiceBus.ServiceBusProcessor":
8382
return new ClientConnectionOptions("cm_servicebus_default_topic/cm_servicebus_subscription_default");
8483
case "Azure.Messaging.ServiceBus.ServiceBusProcessor$private":
8584
return new ClientConnectionOptions("cm_servicebus_topic_private/cm_servicebus_subscription_private");
8685
case "Azure.Storage.Blobs.BlobContainerClient":
87-
return new ClientConnectionOptions(new($"https://{Id}.blob.core.windows.net/{instanceId??"default"}"), Credential);
86+
return new ClientConnectionOptions(new($"https://{Id}.blob.core.windows.net/{instanceId ?? "default"}"), Credential);
8887
case "Azure.AI.OpenAI.AzureOpenAIClient":
8988
return new ClientConnectionOptions(new($"https://{Id}.openai.azure.com"), Credential);
9089
case "OpenAI.Chat.ChatClient":

sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.net8.0.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ public FeatureCollection() { }
4646
public System.Collections.Generic.IEnumerable<T> FindAll<T>() where T : Azure.Provisioning.CloudMachine.CloudMachineFeature { throw null; }
4747
}
4848
}
49+
namespace Azure.CloudMachine.AppService
50+
{
51+
public partial class AppServiceFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
52+
{
53+
public AppServiceFeature(Azure.Provisioning.AppService.AppServiceSkuDescription? sku = null) { }
54+
public Azure.Provisioning.AppService.AppServiceSkuDescription Sku { get { throw null; } set { } }
55+
protected override Azure.Provisioning.Primitives.ProvisionableResource EmitCore(Azure.CloudMachine.CloudMachineInfrastructure infrastructure) { throw null; }
56+
}
57+
}
4958
namespace Azure.CloudMachine.KeyVault
5059
{
5160
public partial class KeyVaultFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature

sdk/provisioning/Azure.Provisioning.CloudMachine/api/Azure.Provisioning.CloudMachine.netstandard2.0.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ public FeatureCollection() { }
4646
public System.Collections.Generic.IEnumerable<T> FindAll<T>() where T : Azure.Provisioning.CloudMachine.CloudMachineFeature { throw null; }
4747
}
4848
}
49+
namespace Azure.CloudMachine.AppService
50+
{
51+
public partial class AppServiceFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
52+
{
53+
public AppServiceFeature(Azure.Provisioning.AppService.AppServiceSkuDescription? sku = null) { }
54+
public Azure.Provisioning.AppService.AppServiceSkuDescription Sku { get { throw null; } set { } }
55+
protected override Azure.Provisioning.Primitives.ProvisionableResource EmitCore(Azure.CloudMachine.CloudMachineInfrastructure infrastructure) { throw null; }
56+
}
57+
}
4958
namespace Azure.CloudMachine.KeyVault
5059
{
5160
public partial class KeyVaultFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature

sdk/provisioning/Azure.Provisioning.CloudMachine/src/Azure.Provisioning.CloudMachine.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<PackageReference Include="Azure.Provisioning.CognitiveServices" />
1919
<PackageReference Include="Azure.Provisioning.ServiceBus" />
2020
<PackageReference Include="Azure.Provisioning.EventGrid" />
21+
<PackageReference Include="Azure.Provisioning.AppService" />
2122
</ItemGroup>
2223

2324
</Project>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Provisioning.CloudMachine;
5+
using Azure.Provisioning.Expressions;
6+
using Azure.Provisioning.AppService;
7+
using Azure.Provisioning.Primitives;
8+
using Azure.Provisioning.Resources;
9+
10+
namespace Azure.CloudMachine.AppService;
11+
12+
public class AppServiceFeature : CloudMachineFeature
13+
{
14+
public AppServiceSkuDescription Sku { get; set; }
15+
16+
public AppServiceFeature(AppServiceSkuDescription? sku = default)
17+
{
18+
if (sku == default)
19+
{
20+
sku = new AppServiceSkuDescription { Tier = "Free", Name = "F1" };
21+
}
22+
Sku = sku;
23+
}
24+
25+
protected override ProvisionableResource EmitCore(CloudMachineInfrastructure infrastructure)
26+
{
27+
//Add a App Service to the CloudMachine infrastructure.
28+
AppServicePlan hostingPlan = new("cm_hosting_plan")
29+
{
30+
Name = infrastructure.Id,
31+
Sku = Sku,
32+
Kind = "app"
33+
};
34+
infrastructure.AddResource(hostingPlan);
35+
36+
WebSite appService = new("cm_website")
37+
{
38+
Name = infrastructure.Id,
39+
Kind = "app",
40+
Tags = { { "azd-service-name", infrastructure.Id } },
41+
Identity = new()
42+
{
43+
ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned,
44+
UserAssignedIdentities = { { BicepFunction.Interpolate($"{infrastructure.Identity.Id}").Compile().ToString(), new UserAssignedIdentityDetails() } }
45+
},
46+
AppServicePlanId = hostingPlan.Id,
47+
IsHttpsOnly = true,
48+
IsEnabled = true,
49+
SiteConfig = new()
50+
{
51+
IsHttp20Enabled = true,
52+
MinTlsVersion = AppServiceSupportedTlsVersion.Tls1_2,
53+
IsWebSocketsEnabled = true,
54+
AppSettings = new()
55+
{
56+
// This is used by the CloudMachineWorkspace to detect that it is running in a deployed App Service.
57+
// The ClientId is used to create a ManagedIdentityCredential so that it wires up to our CloudMachine user-assigned identity.
58+
new AppServiceNameValuePair
59+
{
60+
Name = "CLOUDMACHINE_MANAGED_IDENTITY_CLIENT_ID",
61+
Value = infrastructure.Identity.ClientId
62+
},
63+
}
64+
}
65+
};
66+
infrastructure.AddResource(appService);
67+
68+
return appService;
69+
}
70+
}

sdk/provisioning/Azure.Provisioning.CloudMachine/src/AzureSdkExtensions/OpenAIFeature.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ protected override ProvisionableResource EmitCore(CloudMachineInfrastructure clo
2727
cloudMachine.PrincipalIdParameter)
2828
);
2929

30+
cloudMachine.AddResource(cloudMachine.CreateRoleAssignment(
31+
cognitiveServices,
32+
cognitiveServices.Id,
33+
CognitiveServicesBuiltInRole.CognitiveServicesOpenAIContributor,
34+
cloudMachine.Identity)
35+
);
36+
3037
Emitted = cognitiveServices;
3138

3239
OpenAIModel? previous = null;

sdk/provisioning/Azure.Provisioning.CloudMachine/src/CDKLevel3/CloudMachineInfrastructure.cs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
using System.Collections.Generic;
1414
using Azure.Provisioning;
1515
using Azure.Provisioning.CloudMachine;
16+
using Azure.Core;
17+
using System.Runtime.CompilerServices;
1618

1719
namespace Azure.CloudMachine;
1820

@@ -221,7 +223,8 @@ public void AddFeature(CloudMachineFeature feature)
221223
public void AddEndpoints<T>()
222224
{
223225
Type endpointsType = typeof(T);
224-
if (!endpointsType.IsInterface) throw new InvalidOperationException("Endpoints type must be an interface.");
226+
if (!endpointsType.IsInterface)
227+
throw new InvalidOperationException("Endpoints type must be an interface.");
225228
Endpoints.Add(endpointsType);
226229
}
227230

@@ -242,33 +245,35 @@ public ProvisioningPlan Build(ProvisioningBuildOptions? context = null)
242245
//Add(PrincipalTypeParameter);
243246
//Add(PrincipalNameParameter);
244247

248+
var storageBlobDataContributor = StorageBuiltInRole.StorageBlobDataContributor;
249+
var storageTableDataContributor = StorageBuiltInRole.StorageTableDataContributor;
250+
var azureServiceBusDataSender = ServiceBusBuiltInRole.AzureServiceBusDataSender;
251+
var azureServiceBusDataOwner = ServiceBusBuiltInRole.AzureServiceBusDataOwner;
252+
245253
_infrastructure.Add(Identity);
246254
_infrastructure.Add(_storage);
247-
_infrastructure.Add(_storage.CreateRoleAssignment(StorageBuiltInRole.StorageBlobDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
248-
_infrastructure.Add(_storage.CreateRoleAssignment(StorageBuiltInRole.StorageTableDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
255+
_infrastructure.Add(_storage.CreateRoleAssignment(storageBlobDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
256+
_infrastructure.Add(CreateRoleAssignment(_storage, _storage.Id, storageBlobDataContributor, Identity));
257+
_infrastructure.Add(_storage.CreateRoleAssignment(storageTableDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
258+
_infrastructure.Add(CreateRoleAssignment(_storage, _storage.Id, storageTableDataContributor, Identity));
249259
_infrastructure.Add(_container);
250260
_infrastructure.Add(_blobs);
251261
_infrastructure.Add(_serviceBusNamespace);
252-
_infrastructure.Add(_serviceBusNamespace.CreateRoleAssignment(ServiceBusBuiltInRole.AzureServiceBusDataOwner, RoleManagementPrincipalType.User, PrincipalIdParameter));
262+
_infrastructure.Add(_serviceBusNamespace.CreateRoleAssignment(azureServiceBusDataOwner, RoleManagementPrincipalType.User, PrincipalIdParameter));
263+
_infrastructure.Add(CreateRoleAssignment(_serviceBusNamespace,_serviceBusNamespace.Id, azureServiceBusDataOwner, Identity));
253264
_infrastructure.Add(_serviceBusNamespaceAuthorizationRule);
254265
_infrastructure.Add(_serviceBusTopic_private);
255266
_infrastructure.Add(_serviceBusTopic_default);
256267
_infrastructure.Add(_serviceBusSubscription_private);
257268
_infrastructure.Add(_serviceBusSubscription_default);
258269

259-
// This is necessary until SystemTopic adds an AssignRole method.
260-
var role = ServiceBusBuiltInRole.AzureServiceBusDataSender;
261-
RoleAssignment roleAssignment = new RoleAssignment("cm_servicebus_role");
262-
roleAssignment.Name = BicepFunction.CreateGuid(_serviceBusNamespace.Id, Identity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString()));
263-
roleAssignment.Scope = new IdentifierExpression(_serviceBusNamespace.BicepIdentifier);
264-
roleAssignment.PrincipalType = RoleManagementPrincipalType.ServicePrincipal;
265-
roleAssignment.RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString());
266-
roleAssignment.PrincipalId = Identity.PrincipalId;
270+
RoleAssignment roleAssignment = CreateRoleAssignment(_serviceBusNamespace, _serviceBusNamespace.Id, azureServiceBusDataSender, Identity);
267271
_infrastructure.Add(roleAssignment);
272+
273+
CreateRoleAssignment(_serviceBusNamespace, _serviceBusNamespace.Id, azureServiceBusDataSender, Identity);
268274
// the role assignment must exist before the system topic event subscription is created.
269275
_eventGridSubscription_blobs.DependsOn.Add(roleAssignment);
270276
_infrastructure.Add(_eventGridSubscription_blobs);
271-
272277
_infrastructure.Add(_eventGridTopic_blobs);
273278

274279
// Placeholders for now.
@@ -283,4 +288,21 @@ public ProvisioningPlan Build(ProvisioningBuildOptions? context = null)
283288

284289
return _infrastructure.Build(context);
285290
}
291+
292+
// Temporary until the bug is fixed in the CDK generator which uses the PrincipalId instead of the Id in BicepFunction.CreateGuid.
293+
internal RoleAssignment CreateRoleAssignment(ProvisionableResource resource, BicepValue<ResourceIdentifier> Id, object role, UserAssignedIdentity identity)
294+
{
295+
if (role is null) throw new ArgumentException("Role must not be null.", nameof(role));
296+
var method = role.GetType().GetMethod("GetBuiltInRoleName", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
297+
string roleName = (string)method!.Invoke(null, [role])!;
298+
299+
return new($"{resource.BicepIdentifier}_{identity.BicepIdentifier}_{roleName}")
300+
{
301+
Name = BicepFunction.CreateGuid(Id, identity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role!.ToString()!)),
302+
Scope = new IdentifierExpression(resource.BicepIdentifier),
303+
PrincipalType = RoleManagementPrincipalType.ServicePrincipal,
304+
RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString()!),
305+
PrincipalId = identity.PrincipalId
306+
};
307+
}
286308
}

sdk/provisioning/Azure.Provisioning.CloudMachine/tests/CloudMachineTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#nullable enable
55

6+
using Azure.CloudMachine.AppService;
67
using Azure.CloudMachine.KeyVault;
78
using Azure.CloudMachine.OpenAI;
89
using NUnit.Framework;
@@ -19,6 +20,7 @@ public void GenerateBicep()
1920
infrastructure.AddFeature(new KeyVaultFeature());
2021
infrastructure.AddFeature(new OpenAIModel("gpt-35-turbo", "0125"));
2122
infrastructure.AddFeature(new OpenAIModel("text-embedding-ada-002", "2", AIModelKind.Embedding));
23+
infrastructure.AddFeature(new AppServiceFeature());
2224
}, exitProcessIfHandled:false);
2325
}
2426

0 commit comments

Comments
 (0)