Skip to content

Commit

Permalink
Implement AppServiceFeature (#47269)
Browse files Browse the repository at this point in the history
  • Loading branch information
christothes authored Nov 22, 2024
1 parent a24e586 commit a0363ff
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 31 deletions.
3 changes: 2 additions & 1 deletion eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@
<PackageReference Update="Azure.Storage.Blobs" Version="12.21.1" />
<PackageReference Update="Azure.Storage.Queues" Version="12.19.1" />
<PackageReference Update="Azure.Storage.Files.Shares" Version="12.19.1" />
<PackageReference Update="Azure.AI.Inference" Version="1.0.0-beta.2" />
<PackageReference Update="Azure.AI.Inference" Version="1.0.0-beta.2" />
<PackageReference Update="Azure.AI.OpenAI" Version="2.0.0" />
<PackageReference Update="Azure.ResourceManager" Version="1.13.0" />
<PackageReference Update="Azure.ResourceManager.AppConfiguration" Version="1.3.2" />
Expand Down Expand Up @@ -158,6 +158,7 @@
<PackageReference Update="Azure.Provisioning.KeyVault" Version="1.0.0" />
<PackageReference Update="Azure.Provisioning.ServiceBus" Version="1.0.0" />
<PackageReference Update="Azure.Provisioning.Storage" Version="1.0.0" />
<PackageReference Update="Azure.Provisioning.AppService" Version="1.0.0" />
<PackageReference Update="Microsoft.Bcl.Numerics" Version="8.0.0" />

<!-- Other approved packages -->
Expand Down
33 changes: 16 additions & 17 deletions sdk/cloudmachine/Azure.CloudMachine/src/CloudMachineWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ namespace Azure.CloudMachine;
/// </summary>
public class CloudMachineWorkspace : ClientWorkspace
{
private TokenCredential Credential { get; } = new ChainedTokenCredential(
new AzureCliCredential(),
new AzureDeveloperCliCredential()
);
private TokenCredential Credential { get; }

/// <summary>
/// The cloud machine ID.
Expand All @@ -42,20 +39,21 @@ public CloudMachineWorkspace(TokenCredential credential = default, IConfiguratio
{
Credential = credential;
}

string cmid;
if (configuration == default)
{
cmid = ReadOrCreateCmid();
}
else
{
cmid = configuration["CloudMachine:ID"];
if (cmid == null)
throw new Exception("CloudMachine:ID configuration value missing");
// This environment variable is set by the CloudMachine App Service feature during provisioning.
Credential = Environment.GetEnvironmentVariable("CLOUDMACHINE_MANAGED_IDENTITY_CLIENT_ID") switch
{
string clientId when !string.IsNullOrEmpty(clientId) => new ManagedIdentityCredential(clientId),
_ => new ChainedTokenCredential(new AzureCliCredential(), new AzureDeveloperCliCredential())
};
}

Id = cmid!;
Id = configuration switch
{
null => ReadOrCreateCmid(),
_ => configuration["CloudMachine:ID"] ?? throw new Exception("CloudMachine:ID configuration value missing")
};
}

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

switch (clientId)
{
Expand All @@ -78,13 +77,13 @@ public override ClientConnectionOptions GetConnectionOptions(Type clientType, st
case "Azure.Messaging.ServiceBus.ServiceBusClient":
return new ClientConnectionOptions(new($"https://{Id}.servicebus.windows.net"), Credential);
case "Azure.Messaging.ServiceBus.ServiceBusSender":
return new ClientConnectionOptions(instanceId?? "cm_servicebus_default_topic");
return new ClientConnectionOptions(instanceId ?? "cm_servicebus_default_topic");
case "Azure.Messaging.ServiceBus.ServiceBusProcessor":
return new ClientConnectionOptions("cm_servicebus_default_topic/cm_servicebus_subscription_default");
case "Azure.Messaging.ServiceBus.ServiceBusProcessor$private":
return new ClientConnectionOptions("cm_servicebus_topic_private/cm_servicebus_subscription_private");
case "Azure.Storage.Blobs.BlobContainerClient":
return new ClientConnectionOptions(new($"https://{Id}.blob.core.windows.net/{instanceId??"default"}"), Credential);
return new ClientConnectionOptions(new($"https://{Id}.blob.core.windows.net/{instanceId ?? "default"}"), Credential);
case "Azure.AI.OpenAI.AzureOpenAIClient":
return new ClientConnectionOptions(new($"https://{Id}.openai.azure.com"), Credential);
case "OpenAI.Chat.ChatClient":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public FeatureCollection() { }
public System.Collections.Generic.IEnumerable<T> FindAll<T>() where T : Azure.Provisioning.CloudMachine.CloudMachineFeature { throw null; }
}
}
namespace Azure.CloudMachine.AppService
{
public partial class AppServiceFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
{
public AppServiceFeature(Azure.Provisioning.AppService.AppServiceSkuDescription? sku = null) { }
public Azure.Provisioning.AppService.AppServiceSkuDescription Sku { get { throw null; } set { } }
protected override Azure.Provisioning.Primitives.ProvisionableResource EmitCore(Azure.CloudMachine.CloudMachineInfrastructure infrastructure) { throw null; }
}
}
namespace Azure.CloudMachine.KeyVault
{
public partial class KeyVaultFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public FeatureCollection() { }
public System.Collections.Generic.IEnumerable<T> FindAll<T>() where T : Azure.Provisioning.CloudMachine.CloudMachineFeature { throw null; }
}
}
namespace Azure.CloudMachine.AppService
{
public partial class AppServiceFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
{
public AppServiceFeature(Azure.Provisioning.AppService.AppServiceSkuDescription? sku = null) { }
public Azure.Provisioning.AppService.AppServiceSkuDescription Sku { get { throw null; } set { } }
protected override Azure.Provisioning.Primitives.ProvisionableResource EmitCore(Azure.CloudMachine.CloudMachineInfrastructure infrastructure) { throw null; }
}
}
namespace Azure.CloudMachine.KeyVault
{
public partial class KeyVaultFeature : Azure.Provisioning.CloudMachine.CloudMachineFeature
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<PackageReference Include="Azure.Provisioning.CognitiveServices" />
<PackageReference Include="Azure.Provisioning.ServiceBus" />
<PackageReference Include="Azure.Provisioning.EventGrid" />
<PackageReference Include="Azure.Provisioning.AppService" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Provisioning.CloudMachine;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.AppService;
using Azure.Provisioning.Primitives;
using Azure.Provisioning.Resources;

namespace Azure.CloudMachine.AppService;

public class AppServiceFeature : CloudMachineFeature
{
public AppServiceSkuDescription Sku { get; set; }

public AppServiceFeature(AppServiceSkuDescription? sku = default)
{
if (sku == default)
{
sku = new AppServiceSkuDescription { Tier = "Free", Name = "F1" };
}
Sku = sku;
}

protected override ProvisionableResource EmitCore(CloudMachineInfrastructure infrastructure)
{
//Add a App Service to the CloudMachine infrastructure.
AppServicePlan hostingPlan = new("cm_hosting_plan")
{
Name = infrastructure.Id,
Sku = Sku,
Kind = "app"
};
infrastructure.AddResource(hostingPlan);

WebSite appService = new("cm_website")
{
Name = infrastructure.Id,
Kind = "app",
Tags = { { "azd-service-name", infrastructure.Id } },
Identity = new()
{
ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned,
UserAssignedIdentities = { { BicepFunction.Interpolate($"{infrastructure.Identity.Id}").Compile().ToString(), new UserAssignedIdentityDetails() } }
},
AppServicePlanId = hostingPlan.Id,
IsHttpsOnly = true,
IsEnabled = true,
SiteConfig = new()
{
IsHttp20Enabled = true,
MinTlsVersion = AppServiceSupportedTlsVersion.Tls1_2,
IsWebSocketsEnabled = true,
AppSettings = new()
{
// This is used by the CloudMachineWorkspace to detect that it is running in a deployed App Service.
// The ClientId is used to create a ManagedIdentityCredential so that it wires up to our CloudMachine user-assigned identity.
new AppServiceNameValuePair
{
Name = "CLOUDMACHINE_MANAGED_IDENTITY_CLIENT_ID",
Value = infrastructure.Identity.ClientId
},
}
}
};
infrastructure.AddResource(appService);

return appService;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ protected override ProvisionableResource EmitCore(CloudMachineInfrastructure clo
cloudMachine.PrincipalIdParameter)
);

cloudMachine.AddResource(cloudMachine.CreateRoleAssignment(
cognitiveServices,
cognitiveServices.Id,
CognitiveServicesBuiltInRole.CognitiveServicesOpenAIContributor,
cloudMachine.Identity)
);

Emitted = cognitiveServices;

OpenAIModel? previous = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
using System.Collections.Generic;
using Azure.Provisioning;
using Azure.Provisioning.CloudMachine;
using Azure.Core;
using System.Runtime.CompilerServices;

namespace Azure.CloudMachine;

Expand Down Expand Up @@ -221,7 +223,8 @@ public void AddFeature(CloudMachineFeature feature)
public void AddEndpoints<T>()
{
Type endpointsType = typeof(T);
if (!endpointsType.IsInterface) throw new InvalidOperationException("Endpoints type must be an interface.");
if (!endpointsType.IsInterface)
throw new InvalidOperationException("Endpoints type must be an interface.");
Endpoints.Add(endpointsType);
}

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

var storageBlobDataContributor = StorageBuiltInRole.StorageBlobDataContributor;
var storageTableDataContributor = StorageBuiltInRole.StorageTableDataContributor;
var azureServiceBusDataSender = ServiceBusBuiltInRole.AzureServiceBusDataSender;
var azureServiceBusDataOwner = ServiceBusBuiltInRole.AzureServiceBusDataOwner;

_infrastructure.Add(Identity);
_infrastructure.Add(_storage);
_infrastructure.Add(_storage.CreateRoleAssignment(StorageBuiltInRole.StorageBlobDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(_storage.CreateRoleAssignment(StorageBuiltInRole.StorageTableDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(_storage.CreateRoleAssignment(storageBlobDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(CreateRoleAssignment(_storage, _storage.Id, storageBlobDataContributor, Identity));
_infrastructure.Add(_storage.CreateRoleAssignment(storageTableDataContributor, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(CreateRoleAssignment(_storage, _storage.Id, storageTableDataContributor, Identity));
_infrastructure.Add(_container);
_infrastructure.Add(_blobs);
_infrastructure.Add(_serviceBusNamespace);
_infrastructure.Add(_serviceBusNamespace.CreateRoleAssignment(ServiceBusBuiltInRole.AzureServiceBusDataOwner, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(_serviceBusNamespace.CreateRoleAssignment(azureServiceBusDataOwner, RoleManagementPrincipalType.User, PrincipalIdParameter));
_infrastructure.Add(CreateRoleAssignment(_serviceBusNamespace,_serviceBusNamespace.Id, azureServiceBusDataOwner, Identity));
_infrastructure.Add(_serviceBusNamespaceAuthorizationRule);
_infrastructure.Add(_serviceBusTopic_private);
_infrastructure.Add(_serviceBusTopic_default);
_infrastructure.Add(_serviceBusSubscription_private);
_infrastructure.Add(_serviceBusSubscription_default);

// This is necessary until SystemTopic adds an AssignRole method.
var role = ServiceBusBuiltInRole.AzureServiceBusDataSender;
RoleAssignment roleAssignment = new RoleAssignment("cm_servicebus_role");
roleAssignment.Name = BicepFunction.CreateGuid(_serviceBusNamespace.Id, Identity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString()));
roleAssignment.Scope = new IdentifierExpression(_serviceBusNamespace.BicepIdentifier);
roleAssignment.PrincipalType = RoleManagementPrincipalType.ServicePrincipal;
roleAssignment.RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString());
roleAssignment.PrincipalId = Identity.PrincipalId;
RoleAssignment roleAssignment = CreateRoleAssignment(_serviceBusNamespace, _serviceBusNamespace.Id, azureServiceBusDataSender, Identity);
_infrastructure.Add(roleAssignment);

CreateRoleAssignment(_serviceBusNamespace, _serviceBusNamespace.Id, azureServiceBusDataSender, Identity);
// the role assignment must exist before the system topic event subscription is created.
_eventGridSubscription_blobs.DependsOn.Add(roleAssignment);
_infrastructure.Add(_eventGridSubscription_blobs);

_infrastructure.Add(_eventGridTopic_blobs);

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

return _infrastructure.Build(context);
}

// Temporary until the bug is fixed in the CDK generator which uses the PrincipalId instead of the Id in BicepFunction.CreateGuid.
internal RoleAssignment CreateRoleAssignment(ProvisionableResource resource, BicepValue<ResourceIdentifier> Id, object role, UserAssignedIdentity identity)
{
if (role is null) throw new ArgumentException("Role must not be null.", nameof(role));
var method = role.GetType().GetMethod("GetBuiltInRoleName", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public);
string roleName = (string)method!.Invoke(null, [role])!;

return new($"{resource.BicepIdentifier}_{identity.BicepIdentifier}_{roleName}")
{
Name = BicepFunction.CreateGuid(Id, identity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role!.ToString()!)),
Scope = new IdentifierExpression(resource.BicepIdentifier),
PrincipalType = RoleManagementPrincipalType.ServicePrincipal,
RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", role.ToString()!),
PrincipalId = identity.PrincipalId
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#nullable enable

using Azure.CloudMachine.AppService;
using Azure.CloudMachine.KeyVault;
using Azure.CloudMachine.OpenAI;
using NUnit.Framework;
Expand All @@ -19,6 +20,7 @@ public void GenerateBicep()
infrastructure.AddFeature(new KeyVaultFeature());
infrastructure.AddFeature(new OpenAIModel("gpt-35-turbo", "0125"));
infrastructure.AddFeature(new OpenAIModel("text-embedding-ada-002", "2", AIModelKind.Embedding));
infrastructure.AddFeature(new AppServiceFeature());
}, exitProcessIfHandled:false);
}

Expand Down

0 comments on commit a0363ff

Please sign in to comment.