Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f601838
Added initial support for app service as a compute environment
davidfowl May 3, 2025
75754d0
Rename AzureContainerAppExecutableExtensions to AzureAppServiceComput…
davidfowl May 3, 2025
d377bda
Add playground sample
davidfowl May 3, 2025
de11dd4
Enhance Azure App Service error messages and skip ignored resources i…
davidfowl May 3, 2025
2517e25
Refactor Azure Cosmos DB configuration to remove access key authentic…
davidfowl May 3, 2025
8584b0c
Remove Redis package references from Azure App Host project
davidfowl May 3, 2025
c12e359
Remove redis urls
davidfowl May 3, 2025
6990e1b
Refactor identity handling in Azure App Service environment extension…
davidfowl May 3, 2025
70ea39b
Remove unused parameter resolution logic for Azure Bicep resources in…
davidfowl May 3, 2025
bca9076
Refactor Key Vault secret handling to streamline resource allocation …
davidfowl May 3, 2025
94ef997
Added test or AsKeyVaultSecret
davidfowl May 5, 2025
d1f17a4
Normalize Key Vault name generation in AsKeyVaultSecret method for co…
davidfowl May 5, 2025
655962f
Update Key Vault secret URI handling to use non-versioned URIs for Ap…
davidfowl May 5, 2025
964202c
Add Azure Bicep modules and update App Service environment references…
davidfowl May 5, 2025
b2cceb5
Moved projects
davidfowl May 5, 2025
770945d
Update parameter names from 'id' to 'planid' for consistency in Bicep…
davidfowl May 5, 2025
2ed723d
Update PackageTags in project file for improved clarity and consistency
davidfowl May 5, 2025
3282b01
Fixed xml
davidfowl May 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Aspire.sln
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
EndProject
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}"
EndProject
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}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -3891,6 +3893,18 @@ Global
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x64.Build.0 = Release|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.ActiveCfg = Release|Any CPU
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3}.Release|x86.Build.0 = Release|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x64.ActiveCfg = Debug|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x64.Build.0 = Debug|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x86.ActiveCfg = Debug|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Debug|x86.Build.0 = Debug|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|Any CPU.Build.0 = Release|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x64.ActiveCfg = Release|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x64.Build.0 = Release|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x86.ActiveCfg = Release|Any CPU
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -4209,6 +4223,7 @@ Global
{8FCA0CFA-7823-6A2F-342A-107A994915B0} = {C424395C-1235-41A4-BF55-07880A04368C}
{30950CEB-2232-F9FC-04FF-ADDCB8AC30A7} = {C424395C-1235-41A4-BF55-07880A04368C}
{6CBA29C8-FF78-4ABC-BEFA-2A53CB4DB2A3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{5DDF8E89-FBBD-4A6F-BF32-7D2140724941} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {47DCFECF-5631-4BDE-A1EC-BE41E90F60C4}
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<PackageVersion Include="Azure.Provisioning" Version="$(AzureProvisiongVersion)" />
<PackageVersion Include="Azure.Provisioning.AppConfiguration" Version="$(AzureProvisiongVersion)" />
<PackageVersion Include="Azure.Provisioning.AppContainers" Version="$(AzureProvisiongVersion)" />
<PackageVersion Include="Azure.Provisioning.AppService" Version="$(AzureProvisiongVersion)" />
<PackageVersion Include="Azure.Provisioning.ApplicationInsights" Version="$(AzureProvisiongVersion)" />
<PackageVersion Include="Azure.Provisioning.ContainerRegistry" Version="$(AzureProvisiongVersion)" />
<PackageVersion Include="Azure.Provisioning.CognitiveServices" Version="$(AzureProvisiongVersion)" />
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<IsPackable>true</IsPackable>
<PackageTags>aspire integration hosting azure</PackageTags>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want any package tags specific for AppService?

<Description>Azure app service resource types for .NET Aspire.</Description>
<PackageIconFullPath>$(SharedDir)Azure_256x.png</PackageIconFullPath>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does app service have its own icon?

<SuppressFinalPackageVersion>true</SuppressFinalPackageVersion>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(SharedDir)BicepFunction2.cs" Link="Provisioning\Utils\BicepFunction2.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Azure.Provisioning.AppService" />
<PackageReference Include="Azure.Provisioning.ContainerRegistry" />
<ProjectReference Include="..\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj" />
<ProjectReference Include="..\Aspire.Hosting.Azure.ContainerRegistry\Aspire.Hosting.Azure.ContainerRegistry.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Azure.Provisioning.AppService;

namespace Aspire.Hosting;

/// <summary>
/// Provides extension methods for publishing compute resources as Azure App Service websites.
/// </summary>
public static class AzureAppServiceComputeResourceExtensions
{
/// <summary>
/// Publishes the specified compute resource as an Azure App Service.
/// </summary>
/// <typeparam name="T">The type of the compute resource.</typeparam>
/// <param name="builder">The compute resource builder.</param>
/// <param name="configure">The configuration action for the App Service WebSite resource.</param>
/// <returns>The updated compute resource builder.</returns>
/// <remarks>
/// This method checks if the application is in publish mode. If it is, it adds the necessary infrastructure
/// for Azure App Service and applies the provided configuration action to the App Service WebSite resource.
/// <example>
/// <code>
/// builder.AddNpmApp("name", "image").PublishAsAzureAppServiceWebsite((infrastructure, app) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work yet, right? Should this be AddProject?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep!

/// {
/// // Configure the App Service WebSite resource here
/// });
/// </code>
/// </example>
/// </remarks>
public static IResourceBuilder<T> PublishAsAzureAppServiceWebsite<T>(this IResourceBuilder<T> builder, Action<AzureResourceInfrastructure, WebSite> configure)
#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.
where T : IComputeResource
#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.
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configure);

if (!builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
{
return builder;
}

return builder.WithAnnotation(new AzureAppServiceWebsiteCustomizationAnnotation(configure));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.AppService;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting.Azure;

internal sealed class AzureAppServiceEnvironmentContext(
ILogger logger,
DistributedApplicationExecutionContext executionContext,
AzureAppServiceEnvironmentResource environment)
{
public ILogger Logger => logger;

public DistributedApplicationExecutionContext ExecutionContext => executionContext;

public AzureAppServiceEnvironmentResource Environment => environment;

private readonly Dictionary<IResource, AzureAppServiceWebsiteContext> _appServices = [];

public AzureAppServiceWebsiteContext GetAppServiceContext(IResource resource)
{
if (!_appServices.TryGetValue(resource, out var context))
{
throw new InvalidOperationException($"App Service context not found for resource {resource.Name}.");
}

return context;
}

public async Task<AzureBicepResource> CreateAppServiceAsync(IResource resource, AzureProvisioningOptions provisioningOptions, CancellationToken cancellationToken)
{
if (!_appServices.TryGetValue(resource, out var context))
{
_appServices[resource] = context = new AzureAppServiceWebsiteContext(resource, this);
await context.ProcessAsync(cancellationToken).ConfigureAwait(false);
}

var provisioningResource = new AzureProvisioningResource(resource.Name, context.BuildWebSite)
{
ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions
};

return provisioningResource;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Azure.AppService;
using Aspire.Hosting.Lifecycle;
using Azure.Provisioning;
using Azure.Provisioning.AppService;
using Azure.Provisioning.ContainerRegistry;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.Roles;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting;

/// <summary>
/// Extensions for adding Azure App Service Environment resources to a distributed application builder.
/// </summary>
public static partial class AzureAppServiceEnvironmentExtensions
{
/// <summary>
/// Adds a azure app service environment resource to the distributed application builder.
/// </summary>
/// <param name="builder">The distributed application builder.</param>
/// <param name="name">The name of the resource.</param>
/// <returns><see cref="IResourceBuilder{T}"/></returns>
public static IResourceBuilder<AzureAppServiceEnvironmentResource> AddAppServiceEnvironment(this IDistributedApplicationBuilder builder, string name)
{
builder.AddAzureProvisioning();
builder.Services.Configure<AzureProvisioningOptions>(options => options.SupportsTargetedRoleAssignments = true);

if (builder.ExecutionContext.IsPublishMode)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this check necessary? The infrastructure class checks in its hook already.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its the same in aca

{
builder.Services.TryAddLifecycleHook<AzureAppServiceInfrastructure>();
}

var resource = new AzureAppServiceEnvironmentResource(name, static infra =>
{
var prefix = infra.AspireResource.Name;
var resource = infra.AspireResource;

var identity = new UserAssignedIdentity(Infrastructure.NormalizeBicepIdentifier($"{prefix}-mi"))
{
};

infra.Add(identity);

// This tells azd to avoid creating infrastructure
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? That's the contract? If there is a parameter named userPrincipalId?

Copy link
Member Author

@davidfowl davidfowl May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep @vhvb1989. I think this is because we also support yaml mode where the app uses none of these types and azd still works.

var userPrincipalId = new ProvisioningParameter(AzureBicepResource.KnownParameters.UserPrincipalId, typeof(string));
infra.Add(userPrincipalId);

var tags = new ProvisioningParameter("tags", typeof(object))
{
Value = new BicepDictionary<string>()
};

infra.Add(tags);

ContainerRegistryService? containerRegistry = null;
if (resource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) && registryReferenceAnnotation.Registry is AzureProvisioningResource registry)
{
containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
}
else
{
containerRegistry = new ContainerRegistryService(Infrastructure.NormalizeBicepIdentifier($"{prefix}_acr"))
{
Sku = new() { Name = ContainerRegistrySkuName.Basic },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should consolidate this across ACA and here, so there is only 1 place to change the defaults of an ACR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, endpoint grouping, and env and arg processing.

Tags = tags
};
}

infra.Add(containerRegistry);

var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, identity);

// There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, identity.Id, pullRa.RoleDefinitionId);
infra.Add(pullRa);

var plan = new AppServicePlan(Infrastructure.NormalizeBicepIdentifier($"{prefix}-asplan"))
{
Sku = new AppServiceSkuDescription
{
Name = "B1",
Tier = "Basic"
},
Kind = "Linux",
IsReserved = true
};

infra.Add(plan);

infra.Add(new ProvisioningOutput("id", typeof(string))
{
Value = plan.Id
});

infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
{
Value = containerRegistry.Name
});

// AZD looks for this output to find the container registry endpoint
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
{
Value = containerRegistry.LoginServer
});

infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
{
Value = identity.Id
});

infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID", typeof(string))
{
Value = identity.ClientId
});
});

if (!builder.ExecutionContext.IsPublishMode)
{
return builder.CreateResourceBuilder(resource);
}

return builder.AddResource(resource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.Azure.AppService;

/// <summary>
/// Represents an Azure App Service Environment resource.
/// </summary>
/// <param name="name">The name of the Azure App Service Environment.</param>
/// <param name="configureInfrastructure">The callback to configure the Azure infrastructure for this resource.</param>
public class AzureAppServiceEnvironmentResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure) :
AzureProvisioningResource(name, configureInfrastructure),
#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.
IComputeEnvironmentResource,
IAzureContainerRegistry
#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.
{
// We don't want these to be public if we end up with an app service
// per compute resource.
internal BicepOutputReference IdOutputReference => new("id", this);
internal BicepOutputReference ContainerRegistryUrl => new("AZURE_CONTAINER_REGISTRY_ENDPOINT", this);
internal BicepOutputReference ContainerRegistryName => new("AZURE_CONTAINER_REGISTRY_NAME", this);
internal BicepOutputReference ContainerRegistryManagedIdentityId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", this);
internal BicepOutputReference ContainerRegistryClientId => new("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID", this);

ReferenceExpression IAzureContainerRegistry.ManagedIdentityId =>
ReferenceExpression.Create($"{ContainerRegistryManagedIdentityId}");

ReferenceExpression IContainerRegistry.Name =>
ReferenceExpression.Create($"{ContainerRegistryName}");

ReferenceExpression IContainerRegistry.Endpoint =>
ReferenceExpression.Create($"{ContainerRegistryUrl}");
}
Loading
Loading