Skip to content

Commit 8e8dbd6

Browse files
committed
Added initial support for app service as a compute environment
This will allow us to start experimenting with cross compute endpoint references (which doesn't work today). - The mirrors what we have with azure container apps pretty closely with some limitations. - Only support for projects in this pass - On support for public http endpoints (we don't do anything with private networking) - Single app service plan, which means you can't scale compute independently - Added tests
1 parent 8239e2d commit 8e8dbd6

14 files changed

+1307
-0
lines changed

Aspire.sln

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
665665
EndProject
666666
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.ContainerRegistry", "src\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj", "{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}"
667667
EndProject
668+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Hosting.Azure.AppService", "src\Aspire.Hosting.Azure.AppService\Aspire.Hosting.Azure.AppService.csproj", "{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}"
669+
EndProject
668670
Global
669671
GlobalSection(SolutionConfigurationPlatforms) = preSolution
670672
Debug|Any CPU = Debug|Any CPU
@@ -3891,6 +3893,18 @@ Global
38913893
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x64.Build.0 = Release|Any CPU
38923894
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.ActiveCfg = Release|Any CPU
38933895
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.Build.0 = Release|Any CPU
3896+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
3897+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|Any CPU.Build.0 = Debug|Any CPU
3898+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x64.ActiveCfg = Debug|Any CPU
3899+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x64.Build.0 = Debug|Any CPU
3900+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x86.ActiveCfg = Debug|Any CPU
3901+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x86.Build.0 = Debug|Any CPU
3902+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|Any CPU.ActiveCfg = Release|Any CPU
3903+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|Any CPU.Build.0 = Release|Any CPU
3904+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x64.ActiveCfg = Release|Any CPU
3905+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x64.Build.0 = Release|Any CPU
3906+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x86.ActiveCfg = Release|Any CPU
3907+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x86.Build.0 = Release|Any CPU
38943908
EndGlobalSection
38953909
GlobalSection(SolutionProperties) = preSolution
38963910
HideSolutionNode = FALSE
@@ -4209,6 +4223,7 @@ Global
42094223
{8FCA0CFA-7823-6A2F-342A-107A994915B0} = {C424395C-1235-41A4-BF55-07880A04368C}
42104224
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7} = {C424395C-1235-41A4-BF55-07880A04368C}
42114225
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
4226+
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
42124227
EndGlobalSection
42134228
GlobalSection(ExtensibilityGlobals) = postSolution
42144229
SolutionGuid = {47DCFECF-5631-4BDE-A1EC-BE41E90F60C4}

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<PackageVersion Include="Azure.Provisioning" Version="$(AzureProvisiongVersion)" />
3333
<PackageVersion Include="Azure.Provisioning.AppConfiguration" Version="$(AzureProvisiongVersion)" />
3434
<PackageVersion Include="Azure.Provisioning.AppContainers" Version="$(AzureProvisiongVersion)" />
35+
<PackageVersion Include="Azure.Provisioning.AppService" Version="$(AzureProvisiongVersion)" />
3536
<PackageVersion Include="Azure.Provisioning.ApplicationInsights" Version="$(AzureProvisiongVersion)" />
3637
<PackageVersion Include="Azure.Provisioning.ContainerRegistry" Version="$(AzureProvisiongVersion)" />
3738
<PackageVersion Include="Azure.Provisioning.CognitiveServices" Version="$(AzureProvisiongVersion)" />

src/Aspire.Hosting.Azure.AppContainers/IContainerRegistry.cs

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
5+
<IsPackable>true</IsPackable>
6+
<PackageTags>aspire integration hosting azure</PackageTags>
7+
<Description>Azure app service resource types for .NET Aspire.</Description>
8+
<PackageIconFullPath>$(SharedDir)Azure_256x.png</PackageIconFullPath>
9+
<SuppressFinalPackageVersion>true</SuppressFinalPackageVersion>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<Compile Include="$(SharedDir)BicepFunction2.cs" Link="Provisioning\Utils\BicepFunction2.cs" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="Azure.Provisioning.AppService" />
18+
<PackageReference Include="Azure.Provisioning.ContainerRegistry" />
19+
<ProjectReference Include="..\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj" />
20+
<ProjectReference Include="..\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj" />
21+
</ItemGroup>
22+
23+
</Project>
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
using Aspire.Hosting.Azure;
6+
using Azure.Provisioning.AppService;
7+
8+
namespace Aspire.Hosting;
9+
10+
/// <summary>
11+
/// Provides extension methods for publishing compute resources as Azure App Service websites.
12+
/// </summary>
13+
public static class AzureContainerAppExecutableExtensions
14+
{
15+
/// <summary>
16+
/// Publishes the specified compute resource as an Azure App Service.
17+
/// </summary>
18+
/// <typeparam name="T">The type of the compute resource.</typeparam>
19+
/// <param name="builder">The compute resource builder.</param>
20+
/// <param name="configure">The configuration action for the App Service WebSite resource.</param>
21+
/// <returns>The updated compute resource builder.</returns>
22+
/// <remarks>
23+
/// This method checks if the application is in publish mode. If it is, it adds the necessary infrastructure
24+
/// for Azure App Service and applies the provided configuration action to the App Service WebSite resource.
25+
/// <example>
26+
/// <code>
27+
/// builder.AddNpmApp("name", "image").PublishAsAzureAppServiceWebsite((infrastructure, app) =>
28+
/// {
29+
/// // Configure the App Service WebSite resource here
30+
/// });
31+
/// </code>
32+
/// </example>
33+
/// </remarks>
34+
public static IResourceBuilder<T> PublishAsAzureAppServiceWebsite<T>(this IResourceBuilder<T> builder, Action<AzureResourceInfrastructure, WebSite> configure)
35+
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
36+
where T : IComputeResource
37+
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
38+
{
39+
ArgumentNullException.ThrowIfNull(builder);
40+
ArgumentNullException.ThrowIfNull(configure);
41+
42+
if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
43+
{
44+
return builder;
45+
}
46+
47+
return builder.WithAnnotation(new AzureAppServiceWebsiteCustomizationAnnotation(configure));
48+
}
49+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
using Aspire.Hosting.Azure.AppService;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Aspire.Hosting.Azure;
9+
10+
internal sealed class AzureAppServiceEnvironmentContext(
11+
ILogger logger,
12+
DistributedApplicationExecutionContext executionContext,
13+
AzureAppServiceEnvironmentResource environment)
14+
{
15+
public ILogger Logger => logger;
16+
17+
public DistributedApplicationExecutionContext ExecutionContext => executionContext;
18+
19+
public AzureAppServiceEnvironmentResource Environment => environment;
20+
21+
private readonly Dictionary<IResource, AzureAppServiceWebsiteContext> _appServices = [];
22+
23+
public AzureAppServiceWebsiteContext GetAppServiceContext(IResource resource)
24+
{
25+
if (!_appServices.TryGetValue(resource, out var context))
26+
{
27+
throw new InvalidOperationException($"App Service context not found for resource {resource.Name}.");
28+
}
29+
30+
return context;
31+
}
32+
33+
public async Task<AzureBicepResource> CreateAppServiceAsync(IResource resource, AzureProvisioningOptions provisioningOptions, CancellationToken cancellationToken)
34+
{
35+
if (!_appServices.TryGetValue(resource, out var context))
36+
{
37+
_appServices[resource] = context = new AzureAppServiceWebsiteContext(resource, this);
38+
await context.ProcessAsync(cancellationToken).ConfigureAwait(false);
39+
}
40+
41+
var provisioningResource = new AzureProvisioningResource(resource.Name, context.BuildWebSite)
42+
{
43+
ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions
44+
};
45+
46+
return provisioningResource;
47+
}
48+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
using Aspire.Hosting.Azure;
6+
using Aspire.Hosting.Azure.AppService;
7+
using Aspire.Hosting.Lifecycle;
8+
using Azure.Provisioning;
9+
using Azure.Provisioning.AppService;
10+
using Azure.Provisioning.ContainerRegistry;
11+
using Azure.Provisioning.Expressions;
12+
using Azure.Provisioning.Roles;
13+
using Microsoft.Extensions.DependencyInjection;
14+
15+
namespace Aspire.Hosting;
16+
17+
/// <summary>
18+
/// Extensions for adding Azure App Service Environment resources to a distributed application builder.
19+
/// </summary>
20+
public static partial class AzureAppServiceEnvironmentExtensions
21+
{
22+
/// <summary>
23+
/// Adds a azure app service environment resource to the distributed application builder.
24+
/// </summary>
25+
/// <param name="builder">The distributed application builder.</param>
26+
/// <param name="name">The name of the resource.</param>
27+
/// <returns><see cref="IResourceBuilder{T}"/></returns>
28+
public static IResourceBuilder<AzureAppServiceEnvironmentResource> AddAppServiceEnvironment(this IDistributedApplicationBuilder builder, string name)
29+
{
30+
builder.AddAzureProvisioning();
31+
builder.Services.Configure<AzureProvisioningOptions>(options => options.SupportsTargetedRoleAssignments = true);
32+
33+
if (builder.ExecutionContext.IsPublishMode)
34+
{
35+
builder.Services.TryAddLifecycleHook<AzureAppServiceInfrastructure>();
36+
}
37+
38+
var resource = new AzureAppServiceEnvironmentResource(name, static infra =>
39+
{
40+
var prefix = infra.AspireResource.Name;
41+
var resource = infra.AspireResource;
42+
43+
var identity = new UserAssignedIdentity(Infrastructure.NormalizeBicepIdentifier($"{prefix}-mi"))
44+
{
45+
};
46+
47+
infra.Add(identity);
48+
49+
// This tells azd to avoid creating infrastructure
50+
var userPrincipalId = new ProvisioningParameter(AzureBicepResource.KnownParameters.UserPrincipalId, typeof(string));
51+
infra.Add(userPrincipalId);
52+
53+
var tags = new ProvisioningParameter("tags", typeof(object))
54+
{
55+
Value = new BicepDictionary<string>()
56+
};
57+
58+
infra.Add(tags);
59+
60+
ContainerRegistryService? containerRegistry = null;
61+
if (resource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource registry)
62+
{
63+
containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
64+
}
65+
else
66+
{
67+
containerRegistry = new ContainerRegistryService(Infrastructure.NormalizeBicepIdentifier($"{prefix}_acr"))
68+
{
69+
Sku = new() { Name = ContainerRegistrySkuName.Basic },
70+
Tags = tags
71+
};
72+
}
73+
74+
infra.Add(containerRegistry);
75+
76+
var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, identity);
77+
78+
// There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
79+
pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, identity.Id, pullRa.RoleDefinitionId);
80+
infra.Add(pullRa);
81+
82+
var plan = new AppServicePlan(Infrastructure.NormalizeBicepIdentifier($"{prefix}-asplan"))
83+
{
84+
Sku = new AppServiceSkuDescription
85+
{
86+
Name = "B1",
87+
Tier = "Basic"
88+
},
89+
Kind = "Linux",
90+
IsReserved = true
91+
};
92+
93+
infra.Add(plan);
94+
95+
infra.Add(new ProvisioningOutput("id", typeof(string))
96+
{
97+
Value = plan.Id
98+
});
99+
100+
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
101+
{
102+
Value = containerRegistry.Name
103+
});
104+
105+
// AZD looks for this output to find the container registry endpoint
106+
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
107+
{
108+
Value = containerRegistry.LoginServer
109+
});
110+
111+
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
112+
{
113+
Value = identity.Id
114+
});
115+
116+
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID", typeof(string))
117+
{
118+
Value = identity.ClientId
119+
});
120+
});
121+
122+
if (!builder.ExecutionContext.IsPublishMode)
123+
{
124+
return builder.CreateResourceBuilder(resource);
125+
}
126+
127+
return builder.AddResource(resource);
128+
}
129+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
6+
namespace Aspire.Hosting.Azure.AppService;
7+
8+
/// <summary>
9+
/// Represents an Azure App Service Environment resource.
10+
/// </summary>
11+
/// <param name="name">The name of the Azure App Service Environment.</param>
12+
/// <param name="configureInfrastructure">The callback to configure the Azure infrastructure for this resource.</param>
13+
public class AzureAppServiceEnvironmentResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
14+
AzureProvisioningResource(name, configureInfrastructure),
15+
#pragma warning disable ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
16+
IComputeEnvironmentResource,
17+
IAzureContainerRegistry
18+
#pragma warning restore ASPIRECOMPUTE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
19+
{
20+
// We don't want these to be public if we end up with an app service
21+
// per compute resource.
22+
internal BicepOutputReference IdOutputReference => new("id", this);
23+
internal BicepOutputReference ContainerRegistryUrl => new("AZURE_CONTAINER_REGISTRY_ENDPOINT", this);
24+
internal BicepOutputReference ContainerRegistryName => new("AZURE_CONTAINER_REGISTRY_NAME", this);
25+
internal BicepOutputReference ContainerRegistryManagedIdentityId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", this);
26+
internal BicepOutputReference ContainerRegistryClientId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID", this);
27+
28+
ReferenceExpression IAzureContainerRegistry.ManagedIdentityId =>
29+
ReferenceExpression.Create($"{ContainerRegistryManagedIdentityId}");
30+
31+
ReferenceExpression IContainerRegistry.Name =>
32+
ReferenceExpression.Create($"{ContainerRegistryName}");
33+
34+
ReferenceExpression IContainerRegistry.Endpoint =>
35+
ReferenceExpression.Create($"{ContainerRegistryUrl}");
36+
}

0 commit comments

Comments
 (0)