From bcdd1f565ae3d98578a175a4010b24dc0853cfc5 Mon Sep 17 00:00:00 2001 From: David Charron Date: Thu, 17 Feb 2022 15:15:35 -0500 Subject: [PATCH 1/4] its working before clean-up --- .../MyStack.cs | 183 ++++++++++++++++++ .../Program.cs | 9 + .../Pulumi.yaml | 3 + .../README.md | 42 ++++ ...rver-privateendpoint-vnet-injection.csproj | 16 ++ 5 files changed, 253 insertions(+) create mode 100644 azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs create mode 100644 azure-cs-sqlserver-privateendpoint-vnet-injection/Program.cs create mode 100644 azure-cs-sqlserver-privateendpoint-vnet-injection/Pulumi.yaml create mode 100644 azure-cs-sqlserver-privateendpoint-vnet-injection/README.md create mode 100644 azure-cs-sqlserver-privateendpoint-vnet-injection/azure-cs-sqlserver-privateendpoint-vnet-injection.csproj diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs b/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs new file mode 100644 index 000000000..859cbc570 --- /dev/null +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs @@ -0,0 +1,183 @@ +// Copyright 2016-2022, Pulumi Corporation. All rights reserved. + +using System; +using System.Threading.Tasks; +using Pulumi; +using AzureNative = Pulumi.AzureNative; +using Resources = Pulumi.AzureNative.Resources; +using Sql = Pulumi.AzureNative.Sql; +using Pulumi.Random; + +class MyStack : Stack +{ + public MyStack() + { + var resourceGroup = new Resources.ResourceGroup("resourceGroup"); + + var password = new Pulumi.Random.RandomPassword("admin-password", new Pulumi.Random.RandomPasswordArgs { Length = 20 }); + + Sql.Server server = new Sql.Server( + "server", + new Sql.ServerArgs + { + AdministratorLogin = "admin-user", + AdministratorLoginPassword = password.Result, + ResourceGroupName = resourceGroup.Name, + ServerName = $"{Pulumi.Deployment.Instance.StackName}", + MinimalTlsVersion = "1.2", + PublicNetworkAccess = "Enabled" + }); + + this.ServerName = server.Name.Apply(servername => $"{servername}.database.windows.net"); + + Sql.Database database = new Sql.Database( + "db", + new Sql.DatabaseArgs + { + DatabaseName = "database", + ServerName = server.Name, + Collation = "SQL_Latin1_General_CP1_CI_AI", + ResourceGroupName = resourceGroup.Name, + Sku = new AzureNative.Sql.Inputs.SkuArgs + { + Capacity = 2, + Family = "Gen5", + Name = "GP_S", /*Serverless*/ + } + }); + + var vnet = new AzureNative.Network.VirtualNetwork("my-network", new AzureNative.Network.VirtualNetworkArgs + { + VirtualNetworkName = "my-network", + ResourceGroupName = resourceGroup.Name, + AddressSpace = new AzureNative.Network.Inputs.AddressSpaceArgs + { + AddressPrefixes = new[] { "10.0.0.0/8" } + } + }); + + var subnet = new AzureNative.Network.Subnet("my-subnet", new AzureNative.Network.SubnetArgs + { + Name = "my-subnet", + ResourceGroupName = resourceGroup.Name, + VirtualNetworkName = vnet.Name, + AddressPrefix = "10.0.0.0/16", + PrivateEndpointNetworkPolicies = "Disabled" + }); + + string privateEndpointName = "SQLServer-PrivateEndpoint"; + var privateEndpoint = new AzureNative.Network.PrivateEndpoint(privateEndpointName, new AzureNative.Network.PrivateEndpointArgs + { + ResourceGroupName = resourceGroup.Name, + PrivateEndpointName = privateEndpointName, + PrivateLinkServiceConnections = + { + new AzureNative.Network.Inputs.PrivateLinkServiceConnectionArgs + { + GroupIds = + { + "sqlServer", + }, + Name = $"{privateEndpointName}-PrivateLinkServiceConnection", + PrivateLinkServiceId = server.Id, + }, + }, + Subnet = new AzureNative.Network.Inputs.SubnetArgs + { + Id = subnet.Id, + }, + }); + + var privateZone = new AzureNative.Network.PrivateZone($"sqlserver-privateZone", new AzureNative.Network.PrivateZoneArgs + { + PrivateZoneName = server.Name.Apply(servername => servername + ".database.windows.net"), + ResourceGroupName = resourceGroup.Name, + Location = "global", + }); + + var privateRecordSet = new AzureNative.Network.PrivateRecordSet($"sqlserver-rivateRecordSet", new AzureNative.Network.PrivateRecordSetArgs + { + ARecords = + { + new AzureNative.Network.Inputs.ARecordArgs + { + Ipv4Address = Output.Tuple(resourceGroup.Name, privateEndpoint.Name) + .Apply(names => + { + return GetPrivateEndpointIP(names.Item1, names.Item2); + }), + }, + }, + PrivateZoneName = privateZone.Name, + RecordType = "A", + RelativeRecordSetName = "@", + ResourceGroupName = resourceGroup.Name, + Ttl = 3600, + }); + + string virtualNetworkLinkName = $"sqlserver-VirtualNetworkLink"; + var virtualNetworkLink = new AzureNative.Network.VirtualNetworkLink(virtualNetworkLinkName, new AzureNative.Network.VirtualNetworkLinkArgs + { + PrivateZoneName = privateZone.Name, + ResourceGroupName = resourceGroup.Name, + RegistrationEnabled = false, + Location = "global", + VirtualNetwork = new AzureNative.Network.Inputs.SubResourceArgs + { + Id = vnet.Id, + }, + VirtualNetworkLinkName = virtualNetworkLinkName + }); + + var privateDnsZoneGroup = new AzureNative.Network.PrivateDnsZoneGroup($"sqlserver-PrivateDnsZoneGroup", new AzureNative.Network.PrivateDnsZoneGroupArgs + { + PrivateDnsZoneConfigs = + { + new AzureNative.Network.Inputs.PrivateDnsZoneConfigArgs + { + Name = privateZone.Name, + PrivateDnsZoneId = privateZone.Id, + }, + }, + PrivateDnsZoneGroupName = privateEndpoint.Name, + PrivateEndpointName = privateEndpoint.Name, + ResourceGroupName = resourceGroup.Name, + }); + + } + + public static async Task GetPrivateEndpointIP(string resourceGroupName, string privateEndpointName) + { + /* Azure api does not fill out the Name property of the NetworkInterfaces object nor does it expand to load the PrivateIPAddress under NetworkInterfaces. + Azure api only fill out the Id property of the NetworkInterfaces object + In order to get the PrivateIPAddress we need to call GetNetworkInterface.. which requires the NetworkInterface Name + Thus we need to derived the NetworkInterfaces Name from its Id + */ + Pulumi.Log.Debug("GetPrivateEndpointIP for " + privateEndpointName); + var privateEndpoint = await AzureNative.Network.GetPrivateEndpoint.InvokeAsync(new AzureNative.Network.GetPrivateEndpointArgs + { + PrivateEndpointName = privateEndpointName, + ResourceGroupName = resourceGroupName, + }); + string nicName = privateEndpoint.NetworkInterfaces[0].Id!; + nicName = nicName.Remove(0, nicName.LastIndexOf(@"/") + 1); + if (privateEndpoint != null && nicName != string.Empty) + { + var nic = await AzureNative.Network.GetNetworkInterface.InvokeAsync(new AzureNative.Network.GetNetworkInterfaceArgs + { + NetworkInterfaceName = nicName, + ResourceGroupName = resourceGroupName, + Expand = "true", + }); + if (nic != null && nic.IpConfigurations != null) + { + return nic.IpConfigurations[0].PrivateIPAddress!; + } + } + return string.Empty; + } + + [Output("serverName")] + public Output ServerName { get; set; } + +} diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/Program.cs b/azure-cs-sqlserver-privateendpoint-vnet-injection/Program.cs new file mode 100644 index 000000000..665d44a76 --- /dev/null +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/Program.cs @@ -0,0 +1,9 @@ +// Copyright 2016-2021, Pulumi Corporation. All rights reserved. + +using System.Threading.Tasks; +using Pulumi; + +class Program +{ + static Task Main() => Deployment.RunAsync(); +} diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/Pulumi.yaml b/azure-cs-sqlserver-privateendpoint-vnet-injection/Pulumi.yaml new file mode 100644 index 000000000..dcb5774b9 --- /dev/null +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/Pulumi.yaml @@ -0,0 +1,3 @@ +name: azure-cs-sqlserver +runtime: dotnet +description: An example of a SQLServer on Azure PaaS diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md b/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md new file mode 100644 index 000000000..456b3fd27 --- /dev/null +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md @@ -0,0 +1,42 @@ +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/azure-cs-sqlserver/README.md) + +# A SQLServer on Azure PaaS + +This example configures [An example of a SQLServer on Azure PaaS](https://docs.microsoft.com/en-us/azure/azure-sql/database/logical-servers). + +In addition to the server itself, a database is configured + +## Running the App + +1. Create a new stack: + + ``` + $ pulumi stack init dev + ``` + +1. Login to Azure CLI (you will be prompted to do this during deployment if you forget this step): + + ``` + $ az login + ``` +1. Set the Azure region location to use: + + ``` + $ pulumi config set azure-native:location westus + ``` + +1. Run `pulumi up` to preview and deploy changes: + + ``` + $ pulumi up + Previewing changes: + ... + + Performing changes: + ... + Resources: + + 5 created + Duration: 3m16s + ``` + +1. Check the deployed sql server and database diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/azure-cs-sqlserver-privateendpoint-vnet-injection.csproj b/azure-cs-sqlserver-privateendpoint-vnet-injection/azure-cs-sqlserver-privateendpoint-vnet-injection.csproj new file mode 100644 index 000000000..24f2a85fb --- /dev/null +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/azure-cs-sqlserver-privateendpoint-vnet-injection.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + enable + + + + + + + + + + From 81fa1a1e4620ddc64b61d9b2a4ebed9aa87c7436 Mon Sep 17 00:00:00 2001 From: David Charron Date: Fri, 18 Feb 2022 08:48:34 -0500 Subject: [PATCH 2/4] Code and readme cleanup --- .../MyStack.cs | 12 ++++++------ .../README.md | 16 +++++++++------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs b/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs index 859cbc570..a866ea842 100644 --- a/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs @@ -25,7 +25,7 @@ public MyStack() ResourceGroupName = resourceGroup.Name, ServerName = $"{Pulumi.Deployment.Instance.StackName}", MinimalTlsVersion = "1.2", - PublicNetworkAccess = "Enabled" + PublicNetworkAccess = "Disabled" }); this.ServerName = server.Name.Apply(servername => $"{servername}.database.windows.net"); @@ -46,9 +46,9 @@ public MyStack() } }); - var vnet = new AzureNative.Network.VirtualNetwork("my-network", new AzureNative.Network.VirtualNetworkArgs + var vnet = new AzureNative.Network.VirtualNetwork("SQLServer-network", new AzureNative.Network.VirtualNetworkArgs { - VirtualNetworkName = "my-network", + VirtualNetworkName = "SQLServer-network", ResourceGroupName = resourceGroup.Name, AddressSpace = new AzureNative.Network.Inputs.AddressSpaceArgs { @@ -56,9 +56,9 @@ public MyStack() } }); - var subnet = new AzureNative.Network.Subnet("my-subnet", new AzureNative.Network.SubnetArgs + var subnet = new AzureNative.Network.Subnet("SQLServer-subnet", new AzureNative.Network.SubnetArgs { - Name = "my-subnet", + Name = "SQLServer-subnet", ResourceGroupName = resourceGroup.Name, VirtualNetworkName = vnet.Name, AddressPrefix = "10.0.0.0/16", @@ -95,7 +95,7 @@ public MyStack() Location = "global", }); - var privateRecordSet = new AzureNative.Network.PrivateRecordSet($"sqlserver-rivateRecordSet", new AzureNative.Network.PrivateRecordSetArgs + var privateRecordSet = new AzureNative.Network.PrivateRecordSet($"sqlserver-privateRecordSet", new AzureNative.Network.PrivateRecordSetArgs { ARecords = { diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md b/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md index 456b3fd27..30a177309 100644 --- a/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md @@ -1,10 +1,12 @@ -[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/azure-cs-sqlserver/README.md) +[![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md) # A SQLServer on Azure PaaS -This example configures [An example of a SQLServer on Azure PaaS](https://docs.microsoft.com/en-us/azure/azure-sql/database/logical-servers). +This example configures [An example of a SQLServer on Azure PaaS connected to a subnet using a private endpoint](https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview). In addition to the server itself, a database is configured +The server is not exposed to the internet and to connect to it you have to use a ressource in the subnet. +A private DNS Zone is configured to point the server record to the private IP (servername.database.windows.net) ## Running the App @@ -20,14 +22,14 @@ In addition to the server itself, a database is configured $ az login ``` 1. Set the Azure region location to use: - + ``` $ pulumi config set azure-native:location westus ``` 1. Run `pulumi up` to preview and deploy changes: - ``` + ``` $ pulumi up Previewing changes: ... @@ -35,8 +37,8 @@ In addition to the server itself, a database is configured Performing changes: ... Resources: - + 5 created - Duration: 3m16s + + 12 created + Duration: 5m16s ``` -1. Check the deployed sql server and database +1. Check the deployed sql server and the private endpoint configuration. From 2ddc88c1e69420ec13b149e21859a6c0c1539f36 Mon Sep 17 00:00:00 2001 From: David Charron Date: Fri, 18 Feb 2022 08:49:18 -0500 Subject: [PATCH 3/4] title fix --- azure-cs-sqlserver-privateendpoint-vnet-injection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md b/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md index 30a177309..05d618f39 100644 --- a/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md @@ -1,6 +1,6 @@ [![Deploy](https://get.pulumi.com/new/button.svg)](https://app.pulumi.com/new?template=https://github.com/pulumi/examples/blob/master/azure-cs-sqlserver-privateendpoint-vnet-injection/README.md) -# A SQLServer on Azure PaaS +# A SQLServer on Azure PaaS secured using a private endpoint This example configures [An example of a SQLServer on Azure PaaS connected to a subnet using a private endpoint](https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview). From ab70ef7935b194cf630bd444620ed516cec5a1e9 Mon Sep 17 00:00:00 2001 From: David Charron Date: Tue, 17 May 2022 15:40:57 -0400 Subject: [PATCH 4/4] Added privatelink prefix to zonename as per https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-dns#azure-services-dns-zone-configuration --- azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs b/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs index a866ea842..bdcdb54f1 100644 --- a/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs +++ b/azure-cs-sqlserver-privateendpoint-vnet-injection/MyStack.cs @@ -90,7 +90,7 @@ public MyStack() var privateZone = new AzureNative.Network.PrivateZone($"sqlserver-privateZone", new AzureNative.Network.PrivateZoneArgs { - PrivateZoneName = server.Name.Apply(servername => servername + ".database.windows.net"), + PrivateZoneName = server.Name.Apply(servername => servername + ".privatelink.database.windows.net"), ResourceGroupName = resourceGroup.Name, Location = "global", });