This post will show you how to deploy a Static Website on a Storage Account protected with Private Endpoint using Terraform:

Define the terraform providers to use#

Create a providers.tf file with the following contents:

 1terraform {
 2  required_version = "> 0.12"
 3  required_providers {
 4    azurerm = {
 5      source  = "azurerm"
 6      version = "~> 2.26"
 7    }
 8  }
 9}
10
11provider "azurerm" {
12  features {}
13  skip_provider_registration = true
14}

Define the variables#

Create a variables.tf file with the following contents:

 1variable "location" {
 2  default = "west europe"
 3}
 4
 5variable "resource_group" {
 6  default = "web-sta-private-endpoint"
 7}
 8
 9variable "sa_name" {
10  default = "webstapecfm"
11}

Define the required resources#

Create a main.tf file with the following contents:

  1# Create Resource Group
  2resource "azurerm_resource_group" "rg" {
  3  name     = var.resource_group
  4  location = var.location
  5}
  6
  7# Create VNet
  8resource "azurerm_virtual_network" "vnet" {
  9  name                = "private-network"
 10  address_space       = ["10.0.0.0/16"]
 11  location            = azurerm_resource_group.rg.location
 12  resource_group_name = azurerm_resource_group.rg.name
 13}
 14
 15# Create the Subnet for the jumpbox.
 16resource "azurerm_subnet" "jump" {
 17  name                 = "jump"
 18  resource_group_name  = azurerm_resource_group.rg.name
 19  virtual_network_name = azurerm_virtual_network.vnet.name
 20  address_prefixes     = ["10.0.1.0/24"]
 21}
 22
 23# Create the Subnet for the private endpoints. This is where the IP of the private endpoint will live.
 24resource "azurerm_subnet" "endpoint" {
 25  name                 = "endpoint"
 26  resource_group_name  = azurerm_resource_group.rg.name
 27  virtual_network_name = azurerm_virtual_network.vnet.name
 28  address_prefixes     = ["10.0.2.0/24"]
 29
 30  enforce_private_link_endpoint_network_policies = true
 31}
 32
 33# Get current public IP. We'll need this so we can access the Storage Account from our PC.
 34data "http" "current_public_ip" {
 35  url = "http://ipinfo.io/json"
 36  request_headers = {
 37    Accept = "application/json"
 38  }
 39}
 40
 41# Create the "private" Storage Account.
 42resource "azurerm_storage_account" "sa" {
 43  name                      = var.sa_name
 44  resource_group_name       = azurerm_resource_group.rg.name
 45  location                  = azurerm_resource_group.rg.location
 46  account_tier              = "Standard"
 47  account_replication_type  = "GRS"
 48  enable_https_traffic_only = true
 49  # We are enabling the firewall only allowing traffic from our PC's public IP.
 50  network_rules {
 51    default_action             = "Deny"
 52    virtual_network_subnet_ids = []
 53    ip_rules = [
 54      jsondecode(data.http.current_public_ip.body).ip
 55    ]
 56  }
 57
 58  static_website {
 59  }
 60}
 61
 62resource "azurerm_storage_blob" "page" {
 63  name                   = "index.html"
 64  storage_account_name   = azurerm_storage_account.sa.name
 65  storage_container_name = "$web"
 66  type                   = "Block"
 67  source                 = "index.html"
 68}
 69
 70# Create the privatelink.web.core.windows.net Private DNS Zone
 71resource "azurerm_private_dns_zone" "private_web" {
 72  name                = "privatelink.web.core.windows.net"
 73  resource_group_name = azurerm_resource_group.rg.name
 74}
 75
 76# Create the Private endpoint. This is where the Storage account gets a private IP inside the VNet.
 77resource "azurerm_private_endpoint" "endpoint_web" {
 78  name                = "sa-endpoint_web"
 79  location            = azurerm_resource_group.rg.location
 80  resource_group_name = azurerm_resource_group.rg.name
 81  subnet_id           = azurerm_subnet.endpoint.id
 82
 83  private_service_connection {
 84    name                           = "sa-privateserviceconnection-web"
 85    private_connection_resource_id = azurerm_storage_account.sa.id
 86    is_manual_connection           = false
 87    subresource_names              = ["web"]
 88  }
 89
 90  private_dns_zone_group {
 91    name                 = "privatelink-file-web-core-windows-net"
 92    private_dns_zone_ids = [azurerm_private_dns_zone.private_web.id]
 93  }
 94}
 95
 96# Link the Private Zone with the VNet
 97resource "azurerm_private_dns_zone_virtual_network_link" "sa_private_web" {
 98  name                  = "test_web"
 99  resource_group_name   = azurerm_resource_group.rg.name
100  private_dns_zone_name = azurerm_private_dns_zone.private_web.name
101  virtual_network_id    = azurerm_virtual_network.vnet.id
102}
103
104# Public IP for the jumpbox
105resource "azurerm_public_ip" "pip" {
106  name                = "jumpbox-ip"
107  location            = azurerm_resource_group.rg.location
108  resource_group_name = azurerm_resource_group.rg.name
109  allocation_method   = "Static"
110}
111
112# NIC for the jumpbox
113resource "azurerm_network_interface" "nic" {
114  name                = "jumpbox-nic"
115  location            = azurerm_resource_group.rg.location
116  resource_group_name = azurerm_resource_group.rg.name
117
118  ip_configuration {
119    name                          = "internal"
120    subnet_id                     = azurerm_subnet.jump.id
121    private_ip_address_allocation = "Dynamic"
122    public_ip_address_id          = azurerm_public_ip.pip.id
123  }
124}
125
126# Create the jumpbox VM
127resource "azurerm_linux_virtual_machine" "jumpbox" {
128  name                = "jumpbox"
129  resource_group_name = azurerm_resource_group.rg.name
130  location            = azurerm_resource_group.rg.location
131  size                = "Standard_F2"
132  admin_username      = "adminuser"
133  network_interface_ids = [
134    azurerm_network_interface.nic.id,
135  ]
136
137  os_disk {
138    caching              = "ReadWrite"
139    storage_account_type = "Standard_LRS"
140  }
141
142  admin_ssh_key {
143    username   = "adminuser"
144    public_key = file(pathexpand("~/.ssh/id_rsa.pub"))
145  }
146
147  source_image_reference {
148    publisher = "Canonical"
149    offer     = "UbuntuServer"
150    sku       = "16.04-LTS"
151    version   = "latest"
152  }
153}

Note:

  • The above definition will deploy a jumpbox server with a public IP address so you can access it from your PC. This jumpbox is also deployed in the same VNet as the Storage Account’s Private Endpoint.
  • The definition requires your public key to be in ~/.ssh/id_rsa.pub

Define the outputs#

Create a outputs.tf file with the following contents:

1output "jumpbox_ip" {
2  value = azurerm_public_ip.pip.ip_address
3}
4
5output "sa_name" {
6  value = var.sa_name
7}

Create a simple Web Page#

Create a index.html file with the following contents:

1<!doctype html>
2<html>
3  <head>
4    <title>This is your private static web app.</title>
5  </head>
6  <body>
7    <p>This is your private static web app.</p>
8  </body>
9</html>

Deploy the resources#

Run:

1terraform init
2terraform apply

Test the static website#

Login to the jumpbox server

1$jumpboxIp=$(terraform output jumpbox_ip)
2ssh adminuser@$jumpboxIp

Run the following command, to test the static website:

1curl -i https://<storage account name>.z6.web.core.windows.net/index.html

You should get a response similar to:

 1HTTP/1.1 200 OK
 2Content-Length: 180
 3Content-Type: application/octet-stream
 4Last-Modified: Fri, 17 Sep 2021 11:59:00 GMT
 5Accept-Ranges: bytes
 6ETag: "0x8D979D28981C913"
 7Server: Windows-Azure-Web/1.0 Microsoft-HTTPAPI/2.0
 8x-ms-request-id: fb6880b7-a01e-0014-42bb-ab0938000000
 9x-ms-version: 2018-03-28
10Date: Fri, 17 Sep 2021 12:01:41 GMT
11
12<!doctype html>
13<html>
14  <head>
15    <title>This is your private static web app.</title>
16  </head>
17  <body>
18    <p>This is your private static web app.</p>
19  </body>
20</html>

That’s it. Hope it helps!!!

Please find the complete terraform configuration here