Day 3-4: Azure Firewall and Private Endpoints¶
File: terraform/01-foundation/firewall.tf
resource "azurerm_public_ip" "firewall" {
name = "${var.prefix}-fw-pip"
resource_group_name = azurerm_resource_group.shared_services.name
location = azurerm_resource_group.shared_services.location
allocation_method = "Static"
sku = "Standard"
zones = ["1", "2", "3"]
}
resource "azurerm_firewall" "main" {
name = "${var.prefix}-fw"
resource_group_name = azurerm_resource_group.shared_services.name
location = azurerm_resource_group.shared_services.location
sku_name = "AZFW_VNet"
sku_tier = "Premium"
zones = ["1", "2", "3"]
ip_configuration {
name = "fw-ipconfig"
subnet_id = azurerm_subnet.azure_firewall.id
public_ip_address_id = azurerm_public_ip.firewall.id
}
}
resource "azurerm_firewall_policy" "main" {
name = "${var.prefix}-fw-policy"
resource_group_name = azurerm_resource_group.shared_services.name
location = azurerm_resource_group.shared_services.location
sku = "Premium"
intrusion_detection {
mode = "Alert"
}
dns {
servers = ["10.101.1.10", "10.101.1.11"]
proxy_enabled = true
}
}
resource "azurerm_firewall_policy_rule_collection_group" "network_rules" {
name = "network-rules"
firewall_policy_id = azurerm_firewall_policy.main.id
priority = 100
network_rule_collection {
name = "allow-branch-to-azure"
priority = 100
action = "Allow"
rule {
name = "branch-to-sql"
protocols = ["TCP"]
source_addresses = ["192.168.0.0/16", "10.252.0.0/16"]
destination_addresses = ["10.100.3.0/24"]
destination_ports = ["1433"]
}
rule {
name = "branch-to-app-servers"
protocols = ["TCP"]
source_addresses = ["192.168.0.0/16", "10.252.0.0/16"]
destination_addresses = ["10.100.2.0/24"]
destination_ports = ["443", "8080", "8443"]
}
rule {
name = "azure-to-onprem"
protocols = ["Any"]
source_addresses = ["10.100.0.0/16", "10.101.0.0/16"]
destination_addresses = ["10.252.0.0/16"]
destination_ports = ["*"]
}
}
}
File: terraform/01-foundation/private_link.tf
# Azure SQL Server (Public Access Disabled)
resource "azurerm_mssql_server" "main" {
name = "${var.prefix}-sql"
resource_group_name = azurerm_resource_group.prod_workloads.name
location = azurerm_resource_group.prod_workloads.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.sql_admin_password
public_network_access_enabled = false
}
## Azure Storage Account (Public Access Disabled)
resource "azurerm_storage_account" "main" {
name = "${replace(var.prefix, "-", "")}storage"
resource_group_name = azurerm_resource_group.prod_workloads.name
location = azurerm_resource_group.prod_workloads.location
account_tier = "Standard"
account_replication_type = "GRS"
public_network_access_enabled = false
allow_nested_items_to_be_public = false
min_tls_version = "TLS1_2"
network_rules {
default_action = "Deny"
bypass = ["None"]
}
}
## Azure Key Vault
resource "azurerm_key_vault" "main" {
name = "${var.prefix}-kv"
resource_group_name = azurerm_resource_group.shared_services.name
location = azurerm_resource_group.shared_services.location
tenant_id = var.tenant_id
sku_name = "premium"
purge_protection_enabled = true
soft_delete_retention_days = 90
network_acls {
bypass = ["None"]
default_action = "Deny"
}
}
## Private DNS Zones
resource "azurerm_private_dns_zone" "sql" {
name = "privatelink.database.windows.net"
resource_group_name = azurerm_resource_group.networking.name
}
resource "azurerm_private_dns_zone" "storage_blob" {
name = "privatelink.blob.core.windows.net"
resource_group_name = azurerm_resource_group.networking.name
}
resource "azurerm_private_dns_zone" "storage_file" {
name = "privatelink.file.core.windows.net"
resource_group_name = azurerm_resource_group.networking.name
}
resource "azurerm_private_dns_zone" "keyvault" {
name = "privatelink.vaultcore.azure.net"
resource_group_name = azurerm_resource_group.networking.name
}
## DNS Zone VNet Links
resource "azurerm_private_dns_zone_virtual_network_link" "sql_prod" {
name = "sql-link-prod"
resource_group_name = azurerm_resource_group.networking.name
private_dns_zone_name = azurerm_private_dns_zone.sql.name
virtual_network_id = azurerm_virtual_network.prod.id
registration_enabled = false
}
resource "azurerm_private_dns_zone_virtual_network_link" "blob_prod" {
name = "blob-link-prod"
resource_group_name = azurerm_resource_group.networking.name
private_dns_zone_name = azurerm_private_dns_zone.storage_blob.name
virtual_network_id = azurerm_virtual_network.prod.id
registration_enabled = false
}
resource "azurerm_private_dns_zone_virtual_network_link" "kv_prod" {
name = "kv-link-prod"
resource_group_name = azurerm_resource_group.networking.name
private_dns_zone_name = azurerm_private_dns_zone.keyvault.name
virtual_network_id = azurerm_virtual_network.prod.id
registration_enabled = false
}
## Private Endpoints
resource "azurerm_private_endpoint" "sql" {
name = "pe-${var.prefix}-sql"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "psc-sql"
private_connection_resource_id = azurerm_mssql_server.main.id
is_manual_connection = false
subresource_names = ["sqlServer"]
}
private_dns_zone_group {
name = "dns-group-sql"
private_dns_zone_ids = [azurerm_private_dns_zone.sql.id]
}
}
resource "azurerm_private_endpoint" "storage_blob" {
name = "pe-${var.prefix}-blob"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "psc-blob"
private_connection_resource_id = azurerm_storage_account.main.id
is_manual_connection = false
subresource_names = ["blob"]
}
private_dns_zone_group {
name = "dns-group-blob"
private_dns_zone_ids = [azurerm_private_dns_zone.storage_blob.id]
}
}
resource "azurerm_private_endpoint" "keyvault" {
name = "pe-${var.prefix}-kv"
resource_group_name = azurerm_resource_group.networking.name
location = azurerm_resource_group.networking.location
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "psc-kv"
private_connection_resource_id = azurerm_key_vault.main.id
is_manual_connection = false
subresource_names = ["vault"]
}
private_dns_zone_group {
name = "dns-group-kv"
private_dns_zone_ids = [azurerm_private_dns_zone.keyvault.id]
}
}
Deploy Private Link¶
terraform plan -out=tfplan
terraform apply tfplan
## Verify private endpoints
az network private-endpoint list --resource-group abhavtech-networking --output table
## Verify DNS records auto-created
az network private-dns record-set a list \
--resource-group abhavtech-networking \
--zone-name privatelink.database.windows.net \
--output table
## Expected: abhavtech-sql -> 10.100.10.10 (auto-assigned)
Week 2: ExpressRoute Deployment¶
Day 5: Private DNS Conditional Forwarding (On-Premise)¶
Windows DNS Server¶
## Run on Domain Controller / Windows DNS Server
## Required: DNS server must reach Azure DNS (168.63.129.16) via ExpressRoute
Import-Module DnsServer
$azureDnsResolver = "168.63.129.16" # Azure's internal DNS resolver
$zones = @(
"privatelink.database.windows.net",
"privatelink.blob.core.windows.net",
"privatelink.file.core.windows.net",
"privatelink.vaultcore.azure.net"
)
foreach ($zone in $zones) {
## Check if zone already exists
$existing = Get-DnsServerConditionalForwarderZone -Name $zone -ErrorAction SilentlyContinue
if ($existing) {
Write-Host "Zone already exists: $zone - updating"
Set-DnsServerConditionalForwarderZone -Name $zone -MasterServers $azureDnsResolver
} else {
Write-Host "Creating conditional forwarder: $zone"
Add-DnsServerConditionalForwarderZone -Name $zone -MasterServers $azureDnsResolver
}
}
## Verify resolution from on-premise
Write-Host ""
Write-Host "=== DNS Resolution Verification ==="
$testHosts = @(
"abhavtech-sql.database.windows.net",
"abhavtechstorage.blob.core.windows.net",
"abhavtech-kv.vault.azure.net"
)
foreach ($host in $testHosts) {
$result = Resolve-DnsName $host -ErrorAction SilentlyContinue
if ($result) {
$ip = ($result | Where-Object Type -eq 'A').IPAddress
if ($ip -like "10.100.10.*") {
Write-Host "✅ $host -> $ip (private endpoint)"
} else {
Write-Host "❌ $host -> $ip (expected 10.100.10.x)"
}
} else {
Write-Host "❌ $host -> No response"
}
}