File: AzurePrivateEndpointExtensions.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.Network\Aspire.Hosting.Azure.Network.csproj (Aspire.Hosting.Azure.Network)
// 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;
using Azure.Provisioning.Network;
using Azure.Provisioning.PrivateDns;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Provides extension methods for adding Azure Private Endpoint resources to the application model.
/// </summary>
public static class AzurePrivateEndpointExtensions
{
    /// <summary>
    /// Adds an Azure Private Endpoint resource to the subnet.
    /// </summary>
    /// <param name="subnet">The subnet to add the private endpoint to.</param>
    /// <param name="target">The target Azure resource to connect via private link.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{AzurePrivateEndpointResource}"/>.</returns>
    /// <remarks>
    /// <para>
    /// This method automatically creates the Private DNS Zone, VNet Link, and DNS Zone Group
    /// required for private endpoint DNS resolution. Private DNS Zones are shared across
    /// multiple private endpoints that use the same zone name.
    /// </para>
    /// <para>
    /// When a private endpoint is added, the target resource (or its parent) is automatically
    /// configured to deny public network access. To override this behavior, use
    /// <see cref="AzureProvisioningResourceExtensions.ConfigureInfrastructure{T}"/> to customize
    /// the network settings.
    /// </para>
    /// </remarks>
    /// <example>
    /// This example creates a virtual network with a subnet and adds a private endpoint for Azure Storage blobs:
    /// <code>
    /// var vnet = builder.AddAzureVirtualNetwork("vnet");
    /// var peSubnet = vnet.AddSubnet("pe-subnet", "10.0.1.0/24");
    ///
    /// var storage = builder.AddAzureStorage("storage");
    /// var blobs = storage.AddBlobs("blobs");
    ///
    /// peSubnet.AddPrivateEndpoint(blobs);
    /// </code>
    /// </example>
    public static IResourceBuilder<AzurePrivateEndpointResource> AddPrivateEndpoint(
        this IResourceBuilder<AzureSubnetResource> subnet,
        IResourceBuilder<IAzurePrivateEndpointTarget> target)
    {
        ArgumentNullException.ThrowIfNull(subnet);
        ArgumentNullException.ThrowIfNull(target);
 
        var builder = subnet.ApplicationBuilder;
        var name = $"{subnet.Resource.Name}-{target.Resource.Name}-pe";
        var vnet = subnet.Resource.Parent;
 
        var resource = new AzurePrivateEndpointResource(name, subnet.Resource, target.Resource, ConfigurePrivateEndpoint);
 
        if (builder.ExecutionContext.IsRunMode)
        {
            // In run mode, we don't want to add the resource to the builder.
            return builder.CreateResourceBuilder(resource);
        }
 
        // Get or create the shared Private DNS Zone for this zone name
        var zoneName = target.Resource.GetPrivateDnsZoneName();
        var dnsZone = GetOrCreatePrivateDnsZone(builder, zoneName, vnet);
        resource.DnsZone = dnsZone;
 
        // Add annotation to the target's root parent (e.g., storage account) to signal
        // that it should deny public network access.
        // This should only be done in publish mode. In run mode, the target resource
        // needs to be accessible over the public internet so the local app can reach it.
        IResource rootResource = target.Resource;
        while (rootResource is IResourceWithParent parentedResource)
        {
            rootResource = parentedResource.Parent;
        }
        rootResource.Annotations.Add(new PrivateEndpointTargetAnnotation());
 
        return builder.AddResource(resource);
 
        void ConfigurePrivateEndpoint(AzureResourceInfrastructure infra)
        {
            var azureResource = (AzurePrivateEndpointResource)infra.AspireResource;
 
            // Get the shared DNS Zone as an existing resource
            var dnsZone = azureResource.DnsZone!;
            var dnsZoneIdentifier = dnsZone.GetBicepIdentifier();
            var privateDnsZone = PrivateDnsZone.FromExisting(dnsZoneIdentifier);
            privateDnsZone.Name = dnsZone.NameOutput.AsProvisioningParameter(infra);
            infra.Add(privateDnsZone);
 
            // Create the Private Endpoint
            var endpoint = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infra,
                (identifier, name) =>
                {
                    var resource = PrivateEndpoint.FromExisting(identifier);
                    resource.Name = name;
                    return resource;
                },
                (infrastructure) =>
                {
                    var pe = new PrivateEndpoint(infrastructure.AspireResource.GetBicepIdentifier())
                    {
                        Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
                    };
 
                    // Configure subnet
                    pe.Subnet.Id = azureResource.Subnet.Id.AsProvisioningParameter(infrastructure);
 
                    // Configure private link service connection
                    pe.PrivateLinkServiceConnections.Add(
                        new NetworkPrivateLinkServiceConnection
                        {
                            Name = $"{azureResource.Name}-connection",
                            PrivateLinkServiceId = azureResource.Target.Id.AsProvisioningParameter(infrastructure),
                            GroupIds = [.. azureResource.Target.GetPrivateLinkGroupIds()]
                        });
 
                    return pe;
                });
 
            // Create DNS Zone Group on the Private Endpoint
            var dnsZoneGroupIdentifier = $"{endpoint.BicepIdentifier}_dnsgroup";
            var dnsZoneGroup = new PrivateDnsZoneGroup(dnsZoneGroupIdentifier)
            {
                Name = "default",
                Parent = endpoint,
                PrivateDnsZoneConfigs =
                {
                    new PrivateDnsZoneConfig
                    {
                        Name = dnsZoneIdentifier,
                        PrivateDnsZoneId = privateDnsZone.Id
                    }
                }
            };
            infra.Add(dnsZoneGroup);
 
            // Output the Private Endpoint ID for references
            infra.Add(new ProvisioningOutput("id", typeof(string))
            {
                Value = endpoint.Id
            });
 
            // We need to output name so it can be referenced by others.
            infra.Add(new ProvisioningOutput("name", typeof(string)) { Value = endpoint.Name });
        }
    }
 
    /// <summary>
    /// Gets or creates a shared Private DNS Zone for the given zone name and VNet.
    /// </summary>
    private static AzurePrivateDnsZoneResource GetOrCreatePrivateDnsZone(
        IDistributedApplicationBuilder builder,
        string zoneName,
        AzureVirtualNetworkResource vnet)
    {
        // Search for existing DNS Zone with matching zone name
        var existingZone = builder.Resources
            .OfType<AzurePrivateDnsZoneResource>()
            .FirstOrDefault(z => z.ZoneName == zoneName);
 
        AzurePrivateDnsZoneResource dnsZone;
 
        if (existingZone is not null)
        {
            dnsZone = existingZone;
        }
        else
        {
            // Create new DNS Zone resource - use hyphens for resource name
            var zoneResourceName = zoneName.Replace(".", "-");
            dnsZone = new AzurePrivateDnsZoneResource(zoneResourceName, zoneName);
            builder.AddResource(dnsZone);
        }
 
        // Check if VNet Link already exists for this VNet
        if (!dnsZone.VNetLinks.ContainsKey(vnet))
        {
            // Create VNet Link resource
            var linkName = $"{dnsZone.Name}-{vnet.Name}-link";
            var vnetLink = new AzurePrivateDnsZoneVNetLinkResource(linkName, dnsZone, vnet);
            dnsZone.VNetLinks[vnet] = vnetLink;
 
            builder.AddResource(vnetLink).ExcludeFromManifest();
        }
 
        return dnsZone;
    }
}