Skip to content

Commit a30335e

Browse files
authored
Add NAT Gateway and Public IP Address support to Azure Virtual Network (#14413)
* Add NAT Gateway and Public IP Address support to Azure Virtual Network Add AzureNatGatewayResource and AzurePublicIPAddressResource as standalone top-level Azure provisioning resources. A NAT Gateway provides deterministic outbound IP addresses for subnet resources. Key design decisions: - NAT Gateway is a standalone resource (builder.AddNatGateway), not a VNet child - A Public IP Address is auto-created inline in the NAT Gateway bicep module when no explicit PIP is provided via WithPublicIPAddress() - AzurePublicIPAddressResource is a public reusable type for explicit PIP scenarios - Cross-module references use BicepOutputReference + AsProvisioningParameter - Advanced config (idle timeout, zones) available via ConfigureInfrastructure New public API: - AzureNatGatewayResource (.Id, .NameOutput) - AzurePublicIPAddressResource (.Id, .NameOutput) - AddNatGateway() extension on IDistributedApplicationBuilder - AddPublicIPAddress() extension on IDistributedApplicationBuilder - WithPublicIPAddress() extension on IResourceBuilder<AzureNatGatewayResource> - WithNatGateway() extension on IResourceBuilder<AzureSubnetResource> * Address PR feedback Add Tag to auto-created Public IP Address.
1 parent 0e2fce7 commit a30335e

17 files changed

+696
-2
lines changed

playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/Program.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
var containerAppsSubnet = vnet.AddSubnet("container-apps", "10.0.0.0/23");
1212
var privateEndpointsSubnet = vnet.AddSubnet("private-endpoints", "10.0.2.0/27");
1313

14+
// Create a NAT Gateway for deterministic outbound IP on the ACA subnet
15+
var natGateway = builder.AddNatGateway("nat");
16+
containerAppsSubnet.WithNatGateway(natGateway);
17+
1418
// Configure the Container App Environment to use the VNet
1519
builder.AddAzureContainerAppEnvironment("env")
1620
.WithDelegatedSubnet(containerAppsSubnet);

playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/aspire-manifest.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
"resources": {
44
"vnet": {
55
"type": "azure.bicep.v0",
6-
"path": "vnet.module.bicep"
6+
"path": "vnet.module.bicep",
7+
"params": {
8+
"nat_outputs_id": "{nat.outputs.id}"
9+
}
10+
},
11+
"nat": {
12+
"type": "azure.bicep.v0",
13+
"path": "nat.module.bicep"
714
},
815
"env-acr": {
916
"type": "azure.bicep.v0",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
@description('The location for the resource(s) to be deployed.')
2+
param location string = resourceGroup().location
3+
4+
resource nat_pip 'Microsoft.Network/publicIPAddresses@2025-05-01' = {
5+
name: take('nat_pip-${uniqueString(resourceGroup().id)}', 80)
6+
location: location
7+
properties: {
8+
publicIPAllocationMethod: 'Static'
9+
}
10+
sku: {
11+
name: 'Standard'
12+
}
13+
tags: {
14+
'aspire-resource-name': 'nat'
15+
}
16+
}
17+
18+
resource nat 'Microsoft.Network/natGateways@2025-05-01' = {
19+
name: take('nat${uniqueString(resourceGroup().id)}', 24)
20+
location: location
21+
properties: {
22+
publicIpAddresses: [
23+
{
24+
id: nat_pip.id
25+
}
26+
]
27+
}
28+
sku: {
29+
name: 'Standard'
30+
}
31+
tags: {
32+
'aspire-resource-name': 'nat'
33+
}
34+
}
35+
36+
output id string = nat.id
37+
38+
output name string = nat.name

playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/vnet.module.bicep

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
@description('The location for the resource(s) to be deployed.')
22
param location string = resourceGroup().location
33

4+
param nat_outputs_id string
5+
46
resource vnet 'Microsoft.Network/virtualNetworks@2025-05-01' = {
57
name: take('vnet-${uniqueString(resourceGroup().id)}', 64)
68
properties: {
@@ -28,6 +30,9 @@ resource container_apps 'Microsoft.Network/virtualNetworks/subnets@2025-05-01' =
2830
name: 'Microsoft.App/environments'
2931
}
3032
]
33+
natGateway: {
34+
id: nat_outputs_id
35+
}
3136
}
3237
parent: vnet
3338
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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;
7+
using Azure.Provisioning.Network;
8+
using Azure.Provisioning.Resources;
9+
10+
namespace Aspire.Hosting;
11+
12+
/// <summary>
13+
/// Provides extension methods for adding Azure NAT Gateway resources to the application model.
14+
/// </summary>
15+
public static class AzureNatGatewayExtensions
16+
{
17+
/// <summary>
18+
/// Adds an Azure NAT Gateway resource to the application model.
19+
/// </summary>
20+
/// <param name="builder">The builder for the distributed application.</param>
21+
/// <param name="name">The name of the Azure NAT Gateway resource.</param>
22+
/// <returns>A reference to the <see cref="IResourceBuilder{AzureNatGatewayResource}"/>.</returns>
23+
/// <remarks>
24+
/// The NAT Gateway is created with Standard SKU. If no Public IP Address is explicitly associated
25+
/// via <see cref="WithPublicIPAddress"/>, a Public IP Address is automatically created in the
26+
/// NAT Gateway's bicep module with Standard SKU and Static allocation.
27+
/// </remarks>
28+
/// <example>
29+
/// This example creates a NAT Gateway and associates it with a subnet:
30+
/// <code>
31+
/// var natGateway = builder.AddNatGateway("nat");
32+
///
33+
/// var vnet = builder.AddAzureVirtualNetwork("vnet");
34+
/// var subnet = vnet.AddSubnet("aca-subnet", "10.0.0.0/23")
35+
/// .WithNatGateway(natGateway);
36+
/// </code>
37+
/// </example>
38+
public static IResourceBuilder<AzureNatGatewayResource> AddNatGateway(
39+
this IDistributedApplicationBuilder builder,
40+
[ResourceName] string name)
41+
{
42+
ArgumentNullException.ThrowIfNull(builder);
43+
ArgumentException.ThrowIfNullOrEmpty(name);
44+
45+
builder.AddAzureProvisioning();
46+
47+
var resource = new AzureNatGatewayResource(name, ConfigureNatGateway);
48+
49+
if (builder.ExecutionContext.IsRunMode)
50+
{
51+
return builder.CreateResourceBuilder(resource);
52+
}
53+
54+
return builder.AddResource(resource);
55+
}
56+
57+
/// <summary>
58+
/// Associates an explicit Public IP Address resource with the NAT Gateway.
59+
/// </summary>
60+
/// <param name="builder">The NAT Gateway resource builder.</param>
61+
/// <param name="publicIPAddress">The Public IP Address resource to associate.</param>
62+
/// <returns>A reference to the <see cref="IResourceBuilder{AzureNatGatewayResource}"/> for chaining.</returns>
63+
/// <remarks>
64+
/// When an explicit Public IP Address is provided, the NAT Gateway will not auto-create one.
65+
/// </remarks>
66+
/// <example>
67+
/// This example creates a NAT Gateway with an explicit Public IP:
68+
/// <code>
69+
/// var pip = builder.AddPublicIPAddress("nat-pip");
70+
/// var natGateway = builder.AddNatGateway("nat")
71+
/// .WithPublicIPAddress(pip);
72+
/// </code>
73+
/// </example>
74+
public static IResourceBuilder<AzureNatGatewayResource> WithPublicIPAddress(
75+
this IResourceBuilder<AzureNatGatewayResource> builder,
76+
IResourceBuilder<AzurePublicIPAddressResource> publicIPAddress)
77+
{
78+
ArgumentNullException.ThrowIfNull(builder);
79+
ArgumentNullException.ThrowIfNull(publicIPAddress);
80+
81+
builder.Resource.PublicIPAddresses.Add(publicIPAddress.Resource);
82+
return builder;
83+
}
84+
85+
private static void ConfigureNatGateway(AzureResourceInfrastructure infra)
86+
{
87+
var azureResource = (AzureNatGatewayResource)infra.AspireResource;
88+
89+
var natGw = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infra,
90+
(identifier, name) =>
91+
{
92+
var resource = NatGateway.FromExisting(identifier);
93+
resource.Name = name;
94+
return resource;
95+
},
96+
(infrastructure) =>
97+
{
98+
var natGw = new NatGateway(infrastructure.AspireResource.GetBicepIdentifier())
99+
{
100+
SkuName = NatGatewaySkuName.Standard,
101+
Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
102+
};
103+
104+
// If explicit Public IP addresses are provided, reference them via parameters
105+
if (azureResource.PublicIPAddresses.Count > 0)
106+
{
107+
foreach (var pipResource in azureResource.PublicIPAddresses)
108+
{
109+
var pipIdParam = pipResource.Id.AsProvisioningParameter(infrastructure);
110+
natGw.PublicIPAddresses.Add(new WritableSubResource
111+
{
112+
Id = pipIdParam
113+
});
114+
}
115+
}
116+
else
117+
{
118+
// Auto-create a Public IP Address inline
119+
var pip = new PublicIPAddress($"{infrastructure.AspireResource.GetBicepIdentifier()}_pip")
120+
{
121+
Sku = new PublicIPAddressSku()
122+
{
123+
Name = PublicIPAddressSkuName.Standard,
124+
},
125+
PublicIPAllocationMethod = NetworkIPAllocationMethod.Static,
126+
Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
127+
};
128+
infrastructure.Add(pip);
129+
130+
natGw.PublicIPAddresses.Add(new WritableSubResource
131+
{
132+
Id = pip.Id
133+
});
134+
}
135+
136+
return natGw;
137+
});
138+
139+
infra.Add(new ProvisioningOutput("id", typeof(string))
140+
{
141+
Value = natGw.Id
142+
});
143+
144+
infra.Add(new ProvisioningOutput("name", typeof(string))
145+
{
146+
Value = natGw.Name
147+
});
148+
}
149+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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 Azure.Provisioning.Network;
5+
using Azure.Provisioning.Primitives;
6+
7+
namespace Aspire.Hosting.Azure;
8+
9+
/// <summary>
10+
/// Represents an Azure NAT Gateway resource.
11+
/// </summary>
12+
/// <remarks>
13+
/// A NAT Gateway provides outbound internet connectivity for resources in a virtual network subnet.
14+
/// Use <see cref="AzureProvisioningResourceExtensions.ConfigureInfrastructure{T}(ApplicationModel.IResourceBuilder{T}, Action{AzureResourceInfrastructure})"/>
15+
/// to configure specific <see cref="Azure.Provisioning"/> properties.
16+
/// </remarks>
17+
/// <param name="name">The name of the resource.</param>
18+
/// <param name="configureInfrastructure">Callback to configure the Azure NAT Gateway resource.</param>
19+
public class AzureNatGatewayResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
20+
: AzureProvisioningResource(name, configureInfrastructure)
21+
{
22+
/// <summary>
23+
/// Gets the "id" output reference from the Azure NAT Gateway resource.
24+
/// </summary>
25+
public BicepOutputReference Id => new("id", this);
26+
27+
/// <summary>
28+
/// Gets the "name" output reference for the resource.
29+
/// </summary>
30+
public BicepOutputReference NameOutput => new("name", this);
31+
32+
/// <summary>
33+
/// Gets the list of explicit Public IP Address resources associated with this NAT Gateway.
34+
/// </summary>
35+
internal List<AzurePublicIPAddressResource> PublicIPAddresses { get; } = [];
36+
37+
/// <inheritdoc/>
38+
public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra)
39+
{
40+
var bicepIdentifier = this.GetBicepIdentifier();
41+
var resources = infra.GetProvisionableResources();
42+
43+
var existing = resources.OfType<NatGateway>().SingleOrDefault(r => r.BicepIdentifier == bicepIdentifier);
44+
45+
if (existing is not null)
46+
{
47+
return existing;
48+
}
49+
50+
var natGw = NatGateway.FromExisting(bicepIdentifier);
51+
52+
if (!TryApplyExistingResourceAnnotation(this, infra, natGw))
53+
{
54+
natGw.Name = NameOutput.AsProvisioningParameter(infra);
55+
}
56+
57+
infra.Add(natGw);
58+
return natGw;
59+
}
60+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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;
7+
using Azure.Provisioning.Network;
8+
9+
namespace Aspire.Hosting;
10+
11+
/// <summary>
12+
/// Provides extension methods for adding Azure Public IP Address resources to the application model.
13+
/// </summary>
14+
public static class AzurePublicIPAddressExtensions
15+
{
16+
/// <summary>
17+
/// Adds an Azure Public IP Address resource to the application model.
18+
/// </summary>
19+
/// <param name="builder">The builder for the distributed application.</param>
20+
/// <param name="name">The name of the Azure Public IP Address resource.</param>
21+
/// <returns>A reference to the <see cref="IResourceBuilder{AzurePublicIPAddressResource}"/>.</returns>
22+
/// <remarks>
23+
/// The Public IP Address is created with Standard SKU and Static allocation by default.
24+
/// Use <see cref="AzureProvisioningResourceExtensions.ConfigureInfrastructure{T}(IResourceBuilder{T}, Action{AzureResourceInfrastructure})"/>
25+
/// to customize properties such as DNS labels, availability zones, or IP version.
26+
/// </remarks>
27+
/// <example>
28+
/// This example creates a Public IP Address:
29+
/// <code>
30+
/// var pip = builder.AddPublicIPAddress("my-pip");
31+
/// </code>
32+
/// </example>
33+
public static IResourceBuilder<AzurePublicIPAddressResource> AddPublicIPAddress(
34+
this IDistributedApplicationBuilder builder,
35+
[ResourceName] string name)
36+
{
37+
ArgumentNullException.ThrowIfNull(builder);
38+
ArgumentException.ThrowIfNullOrEmpty(name);
39+
40+
builder.AddAzureProvisioning();
41+
42+
var resource = new AzurePublicIPAddressResource(name, ConfigurePublicIPAddress);
43+
44+
if (builder.ExecutionContext.IsRunMode)
45+
{
46+
return builder.CreateResourceBuilder(resource);
47+
}
48+
49+
return builder.AddResource(resource);
50+
}
51+
52+
private static void ConfigurePublicIPAddress(AzureResourceInfrastructure infra)
53+
{
54+
var pip = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infra,
55+
(identifier, name) =>
56+
{
57+
var resource = PublicIPAddress.FromExisting(identifier);
58+
resource.Name = name;
59+
return resource;
60+
},
61+
(infrastructure) =>
62+
{
63+
return new PublicIPAddress(infrastructure.AspireResource.GetBicepIdentifier())
64+
{
65+
Sku = new PublicIPAddressSku()
66+
{
67+
Name = PublicIPAddressSkuName.Standard,
68+
},
69+
PublicIPAllocationMethod = NetworkIPAllocationMethod.Static,
70+
Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
71+
};
72+
});
73+
74+
infra.Add(new ProvisioningOutput("id", typeof(string))
75+
{
76+
Value = pip.Id
77+
});
78+
79+
infra.Add(new ProvisioningOutput("name", typeof(string))
80+
{
81+
Value = pip.Name
82+
});
83+
}
84+
}

0 commit comments

Comments
 (0)