Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
211 changes: 211 additions & 0 deletions quickstart/101-standard-service-endpoint-storage/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
# Create a random name for the resource group using random_pet
resource "random_pet" "rg_name" {
prefix = var.resource_group_name_prefix
}

# Create the resource group using the generated random name
resource "azurerm_resource_group" "rg" {
location = var.resource_group_location
name = random_pet.rg_name.id
}

# Create the virtual network
resource "azurerm_virtual_network" "vnet" {
name = "vnet-1"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}

# Create the public IP address for the NAT gateway
resource "azurerm_public_ip" "nat" {
name = "public-ip-nat"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
allocation_method = "Static"
sku = "Standard"
}

# Create the NAT gateway that provides outbound internet connectivity for the
# virtual machine. The virtual machine has no public IP address of its own.
resource "azurerm_nat_gateway" "nat" {
name = "nat-gateway"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
sku_name = "Standard"
idle_timeout_in_minutes = 4
}

# Associate the public IP address with the NAT gateway
resource "azurerm_nat_gateway_public_ip_association" "nat" {
nat_gateway_id = azurerm_nat_gateway.nat.id
public_ip_address_id = azurerm_public_ip.nat.id
}

# Create the public IP prefix that backs the network identifier. The prefix
# range is what the network security perimeter inbound access rule authorizes.
resource "azurerm_public_ip_prefix" "network_identifier" {
name = "public-ip-prefix"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
prefix_length = 31
sku = "Standard"
ip_version = "IPv4"
}

# Create the public IP address that is used as the network identifier for the
# standard service endpoint. It must be Standard SKU, Static, and IPv4, and it
# must exist before it is associated with the subnet service endpoint.
resource "azurerm_public_ip" "network_identifier" {
name = "public-ip-1"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
allocation_method = "Static"
sku = "Standard"
ip_version = "IPv4"
public_ip_prefix_id = azurerm_public_ip_prefix.network_identifier.id
}

# Create the subnet with a standard service endpoint. The azapi provider is used
# because the service endpoint networkIdentifier property is not yet exposed by
# the azurerm provider. Associating the network identifier with the service
# endpoint is the key configuration step for a standard service endpoint.
resource "azapi_resource" "subnet" {
type = "Microsoft.Network/virtualNetworks/subnets@2025-07-01"
name = "subnet-1"
parent_id = azurerm_virtual_network.vnet.id

body = {
properties = {
addressPrefix = "10.0.0.0/24"

natGateway = {
id = azurerm_nat_gateway.nat.id
}

serviceEndpoints = [
{
service = var.service_endpoint_service

networkIdentifier = {
id = azurerm_public_ip.network_identifier.id
}
}
]
}
}

response_export_values = ["properties.serviceEndpoints"]

depends_on = [azurerm_nat_gateway_public_ip_association.nat]
}

# Create the network interface for the virtual machine. The virtual machine uses
# a private IP address only and reaches Azure Storage through the service endpoint.
resource "azurerm_network_interface" "nic" {
name = "nic-1"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name

ip_configuration {
name = "ipconfig-1"
subnet_id = azapi_resource.subnet.id
private_ip_address_allocation = "Dynamic"
}
}

# Create the Linux virtual machine that represents the IaaS workload
resource "azurerm_linux_virtual_machine" "vm" {
name = "vm-1"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
network_interface_ids = [azurerm_network_interface.nic.id]
size = var.vm_size

os_disk {
name = "osdisk-1"
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}

source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}

computer_name = "vm-1"
admin_username = var.username

admin_ssh_key {
username = var.username
public_key = azapi_resource_action.ssh_public_key_gen.output.publicKey
}

boot_diagnostics {}
}

# Generate a unique suffix for the globally unique storage account name
resource "random_string" "name" {
length = 8
special = false
upper = false
lower = true
numeric = true
}

# Create the Azure Storage account that acts as the PaaS resource behind the
# standard service endpoint. Access is governed by the network security
# perimeter rather than by subnet-scoped storage network rules.
resource "azurerm_storage_account" "sa" {
name = "sa${random_string.name.result}"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
account_tier = "Standard"
account_replication_type = "LRS"
account_kind = "StorageV2"
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = false
}

# Create a file share used to validate connectivity through the service endpoint
resource "azurerm_storage_share" "share" {
name = "fileshare-1"
storage_account_id = azurerm_storage_account.sa.id
quota = 50
}

# Create the network security perimeter that secures the PaaS resource
resource "azurerm_network_security_perimeter" "nsp" {
name = "nsp-1"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
}

# Create a profile in the network security perimeter. Access rules are defined
# on the profile and applied to every resource associated with it.
resource "azurerm_network_security_perimeter_profile" "profile" {
name = "profile-1"
network_security_perimeter_id = azurerm_network_security_perimeter.nsp.id
}

# Associate the storage account with the network security perimeter profile.
# Learning mode logs traffic without blocking it, which is the recommended
# starting point when you evaluate access rules.
resource "azurerm_network_security_perimeter_association" "storage" {
name = "assoc-storage"
network_security_perimeter_profile_id = azurerm_network_security_perimeter_profile.profile.id
resource_id = azurerm_storage_account.sa.id
access_mode = var.nsp_access_mode
}

# Add the IP-based inbound access rule that authorizes traffic from the network
# identifier. The rule matches the public IP prefix that the network identifier
# was allocated from.
resource "azurerm_network_security_perimeter_access_rule" "inbound" {
name = "allow-se-standard"
network_security_perimeter_profile_id = azurerm_network_security_perimeter_profile.profile.id
direction = "Inbound"
address_prefixes = [azurerm_public_ip_prefix.network_identifier.ip_prefix]
}
45 changes: 45 additions & 0 deletions quickstart/101-standard-service-endpoint-storage/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
output "resource_group_name" {
value = azurerm_resource_group.rg.name
}

output "virtual_network_name" {
value = azurerm_virtual_network.vnet.name
}

output "subnet_name" {
value = azapi_resource.subnet.name
}

output "subnet_service_endpoints" {
description = "The service endpoints on the subnet, including the associated network identifier."
value = azapi_resource.subnet.output.properties.serviceEndpoints
}

output "network_identifier_public_ip_name" {
value = azurerm_public_ip.network_identifier.name
}

output "network_identifier_public_ip_address" {
value = azurerm_public_ip.network_identifier.ip_address
}

output "network_identifier_prefix" {
description = "The public IP prefix range authorized by the network security perimeter inbound access rule."
value = azurerm_public_ip_prefix.network_identifier.ip_prefix
}

output "storage_account_name" {
value = azurerm_storage_account.sa.name
}

output "storage_share_name" {
value = azurerm_storage_share.share.name
}

output "network_security_perimeter_name" {
value = azurerm_network_security_perimeter.nsp.name
}

output "virtual_machine_name" {
value = azurerm_linux_virtual_machine.vm.name
}
22 changes: 22 additions & 0 deletions quickstart/101-standard-service-endpoint-storage/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
terraform {
required_version = ">=1.0"

required_providers {
azapi = {
source = "azure/azapi"
version = "~>2.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = "~>4.0"
}
random = {
source = "hashicorp/random"
version = "~>3.0"
}
}
}

provider "azurerm" {
features {}
}
51 changes: 51 additions & 0 deletions quickstart/101-standard-service-endpoint-storage/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Azure standard service endpoint for Azure Storage

This template deploys a [standard service endpoint](https://learn.microsoft.com/azure/private-link/service-endpoint-standard-overview) for Azure Storage.

A standard service endpoint connects IaaS workloads to PaaS resources by using a **network identifier** (a public IP address) and a **network security perimeter**. The network identifier marks service endpoint traffic leaving the subnet, and an IP-based inbound access rule on the perimeter authorizes it. This addresses the scale limits of basic service endpoints, because one public IP address can represent many virtual networks and subnets in the same region and subscription.

## Resources deployed

| Resource | Purpose |
|---|---|
| Virtual network and subnet | Hosts the IaaS workload |
| Public IP prefix and public IP address | The network identifier for the service endpoint |
| Subnet service endpoint (`azapi`) | Associates the network identifier with `Microsoft.Storage` |
| NAT gateway and public IP address | Outbound internet connectivity for the virtual machine |
| Linux virtual machine and network interface | The IaaS workload; private IP address only |
| Storage account and file share | The PaaS resource behind the service endpoint |
| Network security perimeter, profile, and association | Secures the storage account |
| Network security perimeter inbound access rule | Authorizes traffic from the network identifier prefix |

The subnet is created with the `azapi` provider because the service endpoint `networkIdentifier` property is not yet exposed by the `azurerm` provider.

## Prerequisites

Standard service endpoint is in public preview and is gated behind a feature flag. Register the feature in your subscription before you deploy:

```azurecli
az feature register \
--namespace Microsoft.Network \
--name AllowServiceEndpointNetworkIdentifier

az feature show \
--namespace Microsoft.Network \
--name AllowServiceEndpointNetworkIdentifier \
--query "properties.state" \
--output tsv
```

Wait for the state to show `Registered`, then refresh the resource provider registration:

```azurecli
az provider register --namespace Microsoft.Network
```

You also need the `Microsoft.Network/publicIPAddresses/joinServiceEndpointNetworkIdentifier/action` permission, which is included in the **Network Contributor** role.

## Notes

- The network security perimeter association defaults to `Learning` mode, which logs traffic without blocking it. Set `nsp_access_mode` to `Enforced` to apply the access rules.
- The public IP address used as the network identifier must be Standard SKU, Static, and IPv4, and it must exist before it is associated with the service endpoint.
- The default location is `eastus2`.
- The SSH key for the virtual machine is generated at deploy time. Only the public key is exposed in the outputs.
24 changes: 24 additions & 0 deletions quickstart/101-standard-service-endpoint-storage/ssh.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
resource "random_pet" "ssh_key_name" {
prefix = "ssh"
separator = ""
}

resource "azapi_resource_action" "ssh_public_key_gen" {
type = "Microsoft.Compute/sshPublicKeys@2022-11-01"
resource_id = azapi_resource.ssh_public_key.id
action = "generateKeyPair"
method = "POST"

response_export_values = ["publicKey", "privateKey"]
}

resource "azapi_resource" "ssh_public_key" {
type = "Microsoft.Compute/sshPublicKeys@2022-11-01"
name = random_pet.ssh_key_name.id
location = azurerm_resource_group.rg.location
parent_id = azurerm_resource_group.rg.id
}

output "key_data" {
value = azapi_resource_action.ssh_public_key_gen.output.publicKey
}
40 changes: 40 additions & 0 deletions quickstart/101-standard-service-endpoint-storage/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
variable "resource_group_location" {
type = string
default = "eastus2"
description = "Location of the resource group."
}

variable "resource_group_name_prefix" {
type = string
default = "rg"
description = "Prefix of the resource group name that's combined with a random ID so name is unique in your Azure subscription."
}

variable "username" {
type = string
default = "azureadmin"
description = "The username for the local account that will be created on the new virtual machine."
}

variable "vm_size" {
type = string
default = "Standard_DS1_v2"
description = "The size of the virtual machine."
}

variable "service_endpoint_service" {
type = string
default = "Microsoft.Storage"
description = "The service that the standard service endpoint is configured for."
}

variable "nsp_access_mode" {
type = string
default = "Learning"
description = "The access mode of the network security perimeter association. Learning logs traffic without blocking it; Enforced applies the access rules."

validation {
condition = contains(["Learning", "Enforced", "Audit"], var.nsp_access_mode)
error_message = "The nsp_access_mode value must be Learning, Enforced, or Audit."
}
}